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.
Files changed (87) 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 +24 -7
  5. data/ext/cs__common/cs__common.h +12 -2
  6. data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -11
  7. data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -2
  8. data/ext/cs__os_information/cs__os_information.c +31 -0
  9. data/ext/cs__os_information/cs__os_information.h +7 -0
  10. data/ext/{cs__protect_kernel → cs__os_information}/extconf.rb +0 -0
  11. data/lib/contrast/agent/assess/contrast_event.rb +1 -1
  12. data/lib/contrast/agent/assess/contrast_object.rb +1 -4
  13. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
  14. data/lib/contrast/agent/assess/policy/preshift.rb +25 -11
  15. data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
  16. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  17. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -0
  18. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +4 -4
  19. data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
  20. data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
  21. data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -107
  22. data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
  23. data/lib/contrast/agent/assess/property/tagged.rb +15 -132
  24. data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
  25. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +2 -1
  26. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  27. data/lib/contrast/agent/middleware.rb +22 -0
  28. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +0 -1
  29. data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
  30. data/lib/contrast/agent/patching/policy/patch.rb +37 -238
  31. data/lib/contrast/agent/patching/policy/patcher.rb +3 -42
  32. data/lib/contrast/agent/request.rb +5 -3
  33. data/lib/contrast/agent/request_context.rb +32 -11
  34. data/lib/contrast/agent/request_handler.rb +7 -3
  35. data/lib/contrast/agent/rule_set.rb +2 -4
  36. data/lib/contrast/agent/scope.rb +32 -20
  37. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
  38. data/lib/contrast/agent/static_analysis.rb +4 -2
  39. data/lib/contrast/agent/telemetry.rb +129 -0
  40. data/lib/contrast/agent/telemetry_event.rb +34 -0
  41. data/lib/contrast/agent/thread_watcher.rb +43 -14
  42. data/lib/contrast/agent/tracepoint_hook.rb +11 -3
  43. data/lib/contrast/agent/version.rb +1 -1
  44. data/lib/contrast/agent.rb +6 -1
  45. data/lib/contrast/components/api.rb +34 -0
  46. data/lib/contrast/components/app_context.rb +24 -0
  47. data/lib/contrast/components/assess.rb +7 -0
  48. data/lib/contrast/components/config.rb +90 -11
  49. data/lib/contrast/components/contrast_service.rb +6 -0
  50. data/lib/contrast/config/api_configuration.rb +22 -0
  51. data/lib/contrast/config/assess_configuration.rb +1 -0
  52. data/lib/contrast/config/env_variables.rb +25 -0
  53. data/lib/contrast/config/root_configuration.rb +1 -0
  54. data/lib/contrast/config/service_configuration.rb +2 -1
  55. data/lib/contrast/config.rb +1 -0
  56. data/lib/contrast/configuration.rb +3 -0
  57. data/lib/contrast/framework/manager.rb +14 -12
  58. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  59. data/lib/contrast/framework/rails/patch/support.rb +31 -29
  60. data/lib/contrast/logger/application.rb +4 -0
  61. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  62. data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
  63. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  64. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  65. data/lib/contrast/utils/class_util.rb +58 -44
  66. data/lib/contrast/utils/exclude_key.rb +20 -0
  67. data/lib/contrast/utils/io_util.rb +42 -34
  68. data/lib/contrast/utils/lru_cache.rb +45 -0
  69. data/lib/contrast/utils/metrics_hash.rb +59 -0
  70. data/lib/contrast/utils/os.rb +23 -0
  71. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  72. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  73. data/lib/contrast/utils/requests_client.rb +150 -0
  74. data/lib/contrast/utils/ruby_ast_rewriter.rb +1 -1
  75. data/lib/contrast/utils/telemetry.rb +77 -0
  76. data/lib/contrast/utils/telemetry_identifier.rb +137 -0
  77. data/lib/contrast.rb +19 -1
  78. data/resources/assess/policy.json +12 -6
  79. data/resources/deadzone/policy.json +86 -5
  80. data/ruby-agent.gemspec +2 -1
  81. data/service_executables/VERSION +1 -1
  82. data/service_executables/linux/contrast-service +0 -0
  83. data/service_executables/mac/contrast-service +0 -0
  84. metadata +32 -14
  85. data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
  86. data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
  87. 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
@@ -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.size
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