contrast-agent 4.10.0 → 4.13.1
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 +24 -7
- data/ext/cs__common/cs__common.h +12 -2
- data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -11
- data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -2
- 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__protect_kernel → cs__os_information}/extconf.rb +0 -0
- data/lib/contrast/agent/assess/contrast_event.rb +1 -1
- data/lib/contrast/agent/assess/contrast_object.rb +1 -4
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
- data/lib/contrast/agent/assess/policy/preshift.rb +25 -11
- 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/propagator/database_write.rb +2 -0
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +4 -4
- data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
- data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
- data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -107
- data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
- data/lib/contrast/agent/assess/property/tagged.rb +15 -132
- data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +2 -1
- data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
- data/lib/contrast/agent/middleware.rb +22 -0
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +0 -1
- data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
- data/lib/contrast/agent/patching/policy/patch.rb +37 -238
- data/lib/contrast/agent/patching/policy/patcher.rb +3 -42
- data/lib/contrast/agent/request.rb +5 -3
- data/lib/contrast/agent/request_context.rb +32 -11
- data/lib/contrast/agent/request_handler.rb +7 -3
- data/lib/contrast/agent/rule_set.rb +2 -4
- data/lib/contrast/agent/scope.rb +32 -20
- 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/tracepoint_hook.rb +11 -3
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +6 -1
- data/lib/contrast/components/api.rb +34 -0
- data/lib/contrast/components/app_context.rb +24 -0
- data/lib/contrast/components/assess.rb +7 -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/assess_configuration.rb +1 -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/class_util.rb +58 -44
- data/lib/contrast/utils/exclude_key.rb +20 -0
- data/lib/contrast/utils/io_util.rb +42 -34
- data/lib/contrast/utils/lru_cache.rb +45 -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/ruby_ast_rewriter.rb +1 -1
- data/lib/contrast/utils/telemetry.rb +77 -0
- data/lib/contrast/utils/telemetry_identifier.rb +137 -0
- data/lib/contrast.rb +19 -1
- data/resources/assess/policy.json +12 -6
- data/resources/deadzone/policy.json +86 -5
- 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 -14
- data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
- data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
- data/lib/contrast/extension/protect/kernel.rb +0 -29
@@ -0,0 +1,45 @@
|
|
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
|
+
# A LRU(Least Recently Used) Cache store.
|
7
|
+
class LRUCache
|
8
|
+
def initialize capacity = 500
|
9
|
+
raise StandardError 'Capacity must be bigger than 0' if capacity <= 0
|
10
|
+
|
11
|
+
@capacity = capacity
|
12
|
+
@cache = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def [] key
|
16
|
+
val = @cache.delete(key)
|
17
|
+
@cache[key] = val if val
|
18
|
+
val
|
19
|
+
end
|
20
|
+
|
21
|
+
def []= key, value
|
22
|
+
@cache.delete(key)
|
23
|
+
@cache[key] = value
|
24
|
+
@cache.shift if @cache.size > @capacity
|
25
|
+
value # rubocop:disable Lint/Void
|
26
|
+
end
|
27
|
+
|
28
|
+
def keys
|
29
|
+
@cache.keys
|
30
|
+
end
|
31
|
+
|
32
|
+
def key? key
|
33
|
+
@cache.key?(key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def values
|
37
|
+
@cache.values
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
@cache.clear
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
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
|
@@ -0,0 +1,150 @@
|
|
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 'net/http'
|
5
|
+
require 'contrast/components/logger'
|
6
|
+
require 'contrast/utils/object_share'
|
7
|
+
require 'contrast/agent/version'
|
8
|
+
require 'socket'
|
9
|
+
|
10
|
+
module Contrast
|
11
|
+
module Utils
|
12
|
+
# This module creates a Net::HTTP client and initiates a connection to the provided result
|
13
|
+
module RequestsClient
|
14
|
+
ENDPOINT = 'api/v1/telemetry/metrics' # /TelemetryEvent.path
|
15
|
+
|
16
|
+
class << self
|
17
|
+
include Contrast::Components::Logger::InstanceMethods
|
18
|
+
# This method initializes the Net::HTTP client we'll need
|
19
|
+
# @param url [String]
|
20
|
+
# @return [Net::HTTP, nil]
|
21
|
+
def initialize_connection url
|
22
|
+
addr = URI(url)
|
23
|
+
return if addr.host.nil? || addr.port.nil?
|
24
|
+
return if addr.scheme != 'https'
|
25
|
+
|
26
|
+
@_net_http_client = Net::HTTP.new(addr.host, addr.port)
|
27
|
+
@_net_http_client.open_timeout = 5
|
28
|
+
@_net_http_client.read_timeout = 5
|
29
|
+
@_net_http_client.use_ssl = true
|
30
|
+
@_net_http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
31
|
+
@_net_http_client.verify_depth = 5
|
32
|
+
@_net_http_client.start
|
33
|
+
return unless @_net_http_client.started?
|
34
|
+
|
35
|
+
logger.warn('Starting Telemetry connection test')
|
36
|
+
return unless connection_verified? @_net_http_client
|
37
|
+
|
38
|
+
@_net_http_client
|
39
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
40
|
+
logger.warn('Telemetry connection failed', e.message)
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# This method will be responsible for building the request
|
45
|
+
# @param event[Contrast::Agent::TelemetryEvent,Contrast::Agent::StartupMetricsTelemetryEvent]
|
46
|
+
# @return [Net::HTTP::Post]
|
47
|
+
def build_request event
|
48
|
+
return unless valid_event? event
|
49
|
+
|
50
|
+
string_body = event.to_json.to_s
|
51
|
+
header = { 'User-Agent' => "<#{ Contrast::Utils::ObjectShare::RUBY }>-<#{ Contrast::Agent::VERSION }>" }
|
52
|
+
path = ENDPOINT + event.path
|
53
|
+
@_request = Net::HTTP::Post.new(path, header)
|
54
|
+
@_request.body = string_body
|
55
|
+
@_request
|
56
|
+
end
|
57
|
+
|
58
|
+
# This method will create the actual request and send it
|
59
|
+
# @param event[Contrast::Agent::TelemetryEvent]
|
60
|
+
# @param connection[Net::HTTP]
|
61
|
+
def send_request event, connection
|
62
|
+
return if connection.nil? || event.nil?
|
63
|
+
return unless valid_event? event
|
64
|
+
|
65
|
+
req = build_request event
|
66
|
+
connection.request req
|
67
|
+
end
|
68
|
+
|
69
|
+
# This method will handle the response from the tenant
|
70
|
+
# @param res [Net::HTTPResponse]
|
71
|
+
# @return sleep_time [Integer, nil]
|
72
|
+
def handle_response res
|
73
|
+
status_code = res.code.to_i
|
74
|
+
ready_after = if res.to_hash.keys.map(&:downcase).include?('ready-after')
|
75
|
+
res['Ready-After']
|
76
|
+
else
|
77
|
+
60
|
78
|
+
end
|
79
|
+
ready_after if status_code == 429
|
80
|
+
end
|
81
|
+
|
82
|
+
# This method will be responsible for validating the event
|
83
|
+
# @param event[Contrast::Agent::TelemetryEvent,Contrast::Agent::StartupMetricsTelemetryEvent]
|
84
|
+
def valid_event? event
|
85
|
+
return false unless event.cs__is_a?(Contrast::Agent::TelemetryEvent)
|
86
|
+
return false unless event.cs__is_a?(Contrast::Agent::StartupMetricsTelemetryEvent)
|
87
|
+
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Validates connection with Telemetry assigned domain.
|
92
|
+
# If connection is running, SSL certificate of the endpoint is valid, Ip address is resolvable
|
93
|
+
# and response is received without peer's reset or refuse of connection,
|
94
|
+
# then validation returns true. Error handling is in place so that the work of the agent will continue as
|
95
|
+
# normal without Telemetry.
|
96
|
+
#
|
97
|
+
# @param client [Net::HTTP]
|
98
|
+
# @return [Boolean] true | false
|
99
|
+
def connection_verified? client
|
100
|
+
return @_connection_verified unless @_connection_verified.nil?
|
101
|
+
|
102
|
+
# Before RUBY 2.7 there is no #ipaddr
|
103
|
+
ipaddr = if RUBY_VERSION < '2.7.0'
|
104
|
+
socket = TCPSocket.open(client.address, client.port)
|
105
|
+
ipaddr = socket.peeraddr[3]
|
106
|
+
socket.close
|
107
|
+
ipaddr
|
108
|
+
else
|
109
|
+
client.ipaddr
|
110
|
+
end
|
111
|
+
response = client.request(Net::HTTP::Get.new(client.address))
|
112
|
+
verify_cert = OpenSSL::SSL.verify_certificate_identity(client.peer_cert, client.address)
|
113
|
+
resolved = resolved? client.address, ipaddr
|
114
|
+
@_connection_verified = if resolved && response && verify_cert
|
115
|
+
true
|
116
|
+
else
|
117
|
+
false
|
118
|
+
end
|
119
|
+
rescue OpenSSL::SSL::SSLError, Resolv::ResolvError, Errno::ECONNRESET, Errno::ECONNREFUSED,
|
120
|
+
Errno::ETIMEDOUT, Errno::ESHUTDOWN, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, Errno::EISCONN,
|
121
|
+
Errno::ECONNABORTED, Errno::ENETRESET, Errno::ENETUNREACH => e
|
122
|
+
|
123
|
+
logger.warn('Telemetry connection failed', e.message)
|
124
|
+
false
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Resolves the address of the assigned telemetry domain to array of corresponding IPs (if more than one)
|
130
|
+
# and runs a matcher to see if current connection IP is in the list.
|
131
|
+
# This is called within #verify_connection, if called on it's own there will be no
|
132
|
+
# error handling.
|
133
|
+
#
|
134
|
+
# @param address [String] Human friendly address of assigned telemetry domain
|
135
|
+
# @param ipaddr [String] Machine friendly IP address of the assigned telemetry domain
|
136
|
+
# @return[Boolean] true if both addresses are resolved | false if one of the addresses
|
137
|
+
# is non-resolvable
|
138
|
+
def resolved? address, ipaddr
|
139
|
+
return @_resolved unless @_resolved.nil?
|
140
|
+
|
141
|
+
@_resolved = if (addresses = Resolv.getaddresses address)
|
142
|
+
addresses.any? { |addr| addr.include?(ipaddr) }
|
143
|
+
else
|
144
|
+
false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -38,7 +38,7 @@ module Contrast
|
|
38
38
|
return if node.children.all? { |child_node| child_node.type == :str }
|
39
39
|
new_content = +'('
|
40
40
|
idx = 0
|
41
|
-
while idx < node.children.
|
41
|
+
while idx < node.children.length
|
42
42
|
#node.children.each_with_index do |child_node, index|
|
43
43
|
child_node = node.children[idx]
|
44
44
|
# A begin node looks like #{some_code} in ruby, we do a substring
|
@@ -0,0 +1,77 @@
|
|
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/agent/telemetry'
|
5
|
+
require 'contrast/utils/telemetry_identifier'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Utils
|
9
|
+
# Tools for supporting the Telemetry feature
|
10
|
+
module Telemetry
|
11
|
+
DIR = '/ect/contrast/ruby-agent/'.cs__freeze
|
12
|
+
FILE = '.telemetry'.cs__freeze
|
13
|
+
CURRENT_DIR = Dir.pwd.cs__freeze
|
14
|
+
CONFIG_DIR = CURRENT_DIR + '/config/contrast/'.cs__freeze
|
15
|
+
MESSAGE = {
|
16
|
+
disclaimer:
|
17
|
+
"\n===================================================================================================" \
|
18
|
+
"\n\nThe [Contrast Security] [Ruby Agent] " \
|
19
|
+
"collects usage data in order to help us improve compatibility\n" \
|
20
|
+
"and security coverage. The data is anonymous and does not contain application data. It is collected\n" \
|
21
|
+
"by Contrast and is never shared. You can opt-out of telemetry by setting the\n" \
|
22
|
+
"'CONTRAST_AGENT_TELEMETRY_OPTOUT' environment variable to '1' or 'true'.\n\n" \
|
23
|
+
"===================================================================================================\n\n"
|
24
|
+
}.cs__freeze
|
25
|
+
|
26
|
+
# Here we create the .telemetry file. If the file exist we do nothing.
|
27
|
+
#
|
28
|
+
# @return[Boolean, nil] true if file is created, false if file already exist
|
29
|
+
# and nil if Telemetry is disabled or on unsupported OS.
|
30
|
+
def self.create_telemetry_file
|
31
|
+
write_mark_file DIR, FILE, CONFIG_DIR
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.disclaimer
|
35
|
+
@_disclaimer = MESSAGE[:disclaimer]
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
private
|
40
|
+
|
41
|
+
# Create the mark file
|
42
|
+
#
|
43
|
+
# @param dir [String] Directory in which the file is to be created
|
44
|
+
# @param file [String] filename of the mark file
|
45
|
+
# @param config_dir [String] path for the config folder in working directory
|
46
|
+
# @return[Boolean, nil] true if file is created, false if file already exist
|
47
|
+
# and nil if Telemetry is disabled or on unsupported OS.
|
48
|
+
def write_mark_file dir, file, config_dir
|
49
|
+
return unless Contrast::Agent::Telemetry.enabled?
|
50
|
+
return if Contrast::Utils::OS.windows?
|
51
|
+
|
52
|
+
@dir = dir
|
53
|
+
# After macOS Catalina, you can no longer store files or data in the read-only system volume,
|
54
|
+
# nor can we write to the "root" directory ( / ). This results in Errno::EROFS exception.
|
55
|
+
# So for the MacOS we would use the config directory of the instrumented application.
|
56
|
+
@dir = config_dir if Contrast::Utils::OS.mac?
|
57
|
+
return false if File.file? @dir + file
|
58
|
+
return true if touch_marker(@dir, file)
|
59
|
+
|
60
|
+
# If we don't have permission to write to root directory use config instead
|
61
|
+
touch_marker(config_dir, file)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Touches .telemetry file
|
65
|
+
#
|
66
|
+
# @return[Boolean] true if success, false if fails
|
67
|
+
def touch_marker dir, file
|
68
|
+
FileUtils.mkdir_p dir unless Dir.exist? dir
|
69
|
+
FileUtils.touch dir + file
|
70
|
+
File.file? dir + file
|
71
|
+
rescue StandardError => _e
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|