contrast-agent 4.13.1 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +1 -0
  3. data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
  4. data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
  5. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
  6. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
  7. data/lib/contrast/agent/assess/policy/trigger_method.rb +44 -7
  8. data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
  9. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
  10. data/lib/contrast/agent/assess/property/tagged.rb +51 -57
  11. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
  12. data/lib/contrast/agent/metric_telemetry_event.rb +2 -2
  13. data/lib/contrast/agent/middleware.rb +5 -75
  14. data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
  15. data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
  16. data/lib/contrast/agent/patching/policy/patcher.rb +12 -8
  17. data/lib/contrast/agent/reporting/report.rb +21 -0
  18. data/lib/contrast/agent/reporting/reporter.rb +142 -0
  19. data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
  20. data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
  21. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
  22. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
  23. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
  24. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
  25. data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
  26. data/lib/contrast/agent/request.rb +2 -81
  27. data/lib/contrast/agent/request_context.rb +4 -128
  28. data/lib/contrast/agent/request_context_extend.rb +138 -0
  29. data/lib/contrast/agent/response.rb +2 -73
  30. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +39 -16
  31. data/lib/contrast/agent/static_analysis.rb +1 -1
  32. data/lib/contrast/agent/telemetry.rb +15 -7
  33. data/lib/contrast/agent/telemetry_event.rb +8 -9
  34. data/lib/contrast/agent/thread_watcher.rb +31 -5
  35. data/lib/contrast/agent/version.rb +1 -1
  36. data/lib/contrast/agent.rb +15 -0
  37. data/lib/contrast/api/communication/connection_status.rb +10 -7
  38. data/lib/contrast/api/communication/messaging_queue.rb +37 -3
  39. data/lib/contrast/api/communication/response_processor.rb +15 -8
  40. data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
  41. data/lib/contrast/api/communication/socket.rb +6 -8
  42. data/lib/contrast/api/communication/socket_client.rb +29 -12
  43. data/lib/contrast/api/communication/speedracer.rb +37 -1
  44. data/lib/contrast/api/communication/tcp_socket.rb +4 -3
  45. data/lib/contrast/api/communication/unix_socket.rb +1 -0
  46. data/lib/contrast/api/decorators/finding.rb +45 -0
  47. data/lib/contrast/components/api.rb +56 -0
  48. data/lib/contrast/components/app_context.rb +10 -65
  49. data/lib/contrast/components/app_context_extend.rb +78 -0
  50. data/lib/contrast/components/base.rb +23 -0
  51. data/lib/contrast/components/config.rb +8 -8
  52. data/lib/contrast/components/contrast_service.rb +5 -0
  53. data/lib/contrast/components/sampling.rb +2 -2
  54. data/lib/contrast/config/agent_configuration.rb +1 -1
  55. data/lib/contrast/config/api_configuration.rb +9 -4
  56. data/lib/contrast/config/api_proxy_configuration.rb +14 -0
  57. data/lib/contrast/config/application_configuration.rb +2 -3
  58. data/lib/contrast/config/assess_configuration.rb +3 -3
  59. data/lib/contrast/config/base_configuration.rb +17 -28
  60. data/lib/contrast/config/certification_configuration.rb +15 -0
  61. data/lib/contrast/config/env_variables.rb +2 -9
  62. data/lib/contrast/config/heap_dump_configuration.rb +6 -6
  63. data/lib/contrast/config/inventory_configuration.rb +1 -5
  64. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  65. data/lib/contrast/config/request_audit_configuration.rb +18 -0
  66. data/lib/contrast/config/ruby_configuration.rb +6 -6
  67. data/lib/contrast/config/service_configuration.rb +1 -2
  68. data/lib/contrast/config.rb +0 -1
  69. data/lib/contrast/configuration.rb +1 -2
  70. data/lib/contrast/extension/assess/array.rb +5 -7
  71. data/lib/contrast/framework/manager.rb +8 -32
  72. data/lib/contrast/framework/manager_extend.rb +50 -0
  73. data/lib/contrast/framework/rails/railtie.rb +1 -1
  74. data/lib/contrast/framework/sinatra/support.rb +2 -1
  75. data/lib/contrast/logger/log.rb +8 -103
  76. data/lib/contrast/utils/assess/property/tagged_utils.rb +23 -0
  77. data/lib/contrast/utils/assess/tracking_util.rb +20 -15
  78. data/lib/contrast/utils/assess/trigger_method_utils.rb +1 -1
  79. data/lib/contrast/utils/class_util.rb +18 -14
  80. data/lib/contrast/utils/findings.rb +62 -0
  81. data/lib/contrast/utils/hash_digest.rb +10 -73
  82. data/lib/contrast/utils/hash_digest_extend.rb +86 -0
  83. data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
  84. data/lib/contrast/utils/heap_dump_util.rb +2 -65
  85. data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
  86. data/lib/contrast/utils/io_util.rb +1 -1
  87. data/lib/contrast/utils/log_utils.rb +108 -0
  88. data/lib/contrast/utils/middleware_utils.rb +87 -0
  89. data/lib/contrast/utils/net_http_base.rb +158 -0
  90. data/lib/contrast/utils/object_share.rb +1 -0
  91. data/lib/contrast/utils/request_utils.rb +88 -0
  92. data/lib/contrast/utils/response_utils.rb +97 -0
  93. data/lib/contrast/utils/substitution_utils.rb +167 -0
  94. data/lib/contrast/utils/tag_util.rb +9 -9
  95. data/lib/contrast/utils/telemetry.rb +4 -2
  96. data/lib/contrast/utils/telemetry_client.rb +90 -0
  97. data/lib/contrast/utils/telemetry_identifier.rb +17 -24
  98. data/ruby-agent.gemspec +5 -5
  99. metadata +48 -23
  100. data/lib/contrast/config/default_value.rb +0 -17
  101. data/lib/contrast/utils/requests_client.rb +0 -150
@@ -7,6 +7,8 @@ require 'contrast/agent/response'
7
7
  require 'contrast/agent/inventory/database_config'
8
8
  require 'contrast/components/logger'
9
9
  require 'contrast/components/scope'
10
+ require 'contrast/utils/request_utils'
11
+ require 'contrast/agent/request_context_extend'
10
12
 
11
13
  module Contrast
12
14
  module Agent
@@ -27,6 +29,8 @@ module Contrast
27
29
  class RequestContext
28
30
  include Contrast::Components::Logger::InstanceMethods
29
31
  include Contrast::Components::Scope::InstanceMethods
32
+ include Contrast::Utils::RequestUtils
33
+ include Contrast::Agent::RequestContextExtend
30
34
 
31
35
  EMPTY_INPUT_ANALYSIS_PB = Contrast::Api::Settings::InputAnalysis.new
32
36
 
@@ -100,98 +104,6 @@ module Contrast
100
104
  ::Contrast::ASSESS.enabled?
101
105
  end
102
106
 
103
- # Convert the discovered route for this request to appropriate forms and disseminate it to those locations
104
- # where it is necessary for our route coverage and finding vulnerability discovery features to function.
105
- #
106
- # @param route [Contrast::Api::Dtm::RouteCoverage, nil] the route of the current request, as determined from the
107
- # framework
108
- def append_route_coverage route
109
- return unless route
110
-
111
- # For our findings
112
- @route = route
113
-
114
- # For SR findings
115
- @activity.routes << route
116
-
117
- # For TS routes
118
- @observed_route.signature = route.route
119
- @observed_route.verb = route.verb
120
- @observed_route.url = route.url if route.url
121
- @request.route = route
122
- @request.observed_route = @observed_route
123
- end
124
-
125
- # Collect the results for the given rule with the given action
126
- #
127
- # @param rule [String] the id of the rule to which the results apply
128
- # @param response_type [Symbol] the result of the response, matching a value of
129
- # Contrast::Api::Dtm::AttackResult::ResponseType
130
- # @return [Array<Contrast::Api::Dtm::AttackResult>]
131
- def results_for rule, response_type = nil
132
- if response_type.nil?
133
- activity.results.select { |r| r.rule_id == rule }
134
- else
135
- activity.results.select { |r| r.rule_id == rule && r.response == response_type }
136
- end
137
- end
138
-
139
- def service_extract_request
140
- return false unless ::Contrast::AGENT.enabled?
141
- return false unless ::Contrast::PROTECT.enabled?
142
- return false if @do_not_track
143
-
144
- service_response = Contrast::Agent.messaging_queue.send_event_immediately(@activity.http_request)
145
- return false unless service_response
146
-
147
- handle_protect_state(service_response)
148
- ia = service_response.input_analysis
149
- if ia
150
- if logger.trace?
151
- logger.trace('Analysis from Contrast Service', evaluations: ia.results.length)
152
- logger.trace('Results', input_analysis: ia.inspect)
153
- end
154
- @speedracer_input_analysis = ia
155
- speedracer_input_analysis.request = request
156
- else
157
- logger.trace('Analysis from Contrast Service was empty.')
158
- false
159
- end
160
- rescue Contrast::SecurityException => e
161
- raise e
162
- rescue StandardError => e
163
- logger.warn('Unable to extract Contrast Service information from request', e)
164
- false
165
- end
166
-
167
- # NOTE: this method is only used as a backstop if Speedracer sends Input Evaluations when the protect state
168
- # indicates a security exception should be thrown. This method ensures that the attack reports are generated.
169
- # Normally these should be generated on Speedracer for any attacks detected during prefilter.
170
- #
171
- # @param agent_settings [Contrast::Api::Settings::AgentSettings]
172
- def handle_protect_state agent_settings
173
- return unless agent_settings&.protect_state
174
-
175
- state = agent_settings.protect_state
176
- @uuid = state.uuid
177
- @do_not_track = true unless state.track_request
178
- return unless state.security_exception
179
-
180
- # If Contrast Service has NOT handled the input analysis, handle them here
181
- build_attack_results(agent_settings)
182
- logger.debug('Contrast Service said to block this request')
183
- raise Contrast::SecurityException.new(nil, (state.security_message || 'Blocking suspicious behavior'))
184
- end
185
-
186
- # append anything we've learned to the request seen message this is the sum-total of all inventory information
187
- # that has been accumulated since the last request
188
- def extract_after rack_response
189
- @response = Contrast::Agent::Response.new(rack_response)
190
- activity.http_response = @response.dtm if @sample_res
191
- rescue StandardError => e
192
- logger.error('Unable to extract information after request', e)
193
- end
194
-
195
107
  def add_property key, value
196
108
  @_properties[key] = value
197
109
  end
@@ -205,42 +117,6 @@ module Contrast
205
117
  @server_activity = Contrast::Api::Dtm::ServerActivity.new
206
118
  @observed_route = Contrast::Api::Dtm::ObservedRoute.new
207
119
  end
208
-
209
- private
210
-
211
- # Generate attack results directly from any evaluations on the agent settings object.
212
- #
213
- # @param agent_settings [Contrast::Api::Settings::AgentSettings]
214
- def build_attack_results agent_settings
215
- return unless agent_settings&.input_analysis&.results&.any?
216
-
217
- attack_results_by_rule = {}
218
- agent_settings.input_analysis.results.each do |ia_result|
219
- rule_id = ia_result.rule_id
220
- rule = ::Contrast::PROTECT.rule(rule_id)
221
- next unless rule
222
-
223
- if logger.debug?
224
- logger.debug('Building attack result from Contrast Service input analysis result',
225
- result: ia_result.inspect)
226
- end
227
-
228
- attack_result = if rule.mode == :BLOCK
229
- # special case for rules (like reflected xss) that used to have an infilter / block mode
230
- # but now are just block at perimeter
231
- rule.build_attack_with_match(self, ia_result, attack_results_by_rule[rule_id],
232
- ia_result.value)
233
- else
234
- rule.build_attack_without_match(self, ia_result, attack_results_by_rule[rule_id])
235
- end
236
- attack_results_by_rule[rule_id] = attack_result
237
- end
238
-
239
- attack_results_by_rule.each_pair do |_, attack_result|
240
- logger.info('Blocking attack result', rule: attack_result.rule_id)
241
- activity.results << attack_result
242
- end
243
- end
244
120
  end
245
121
  end
246
122
  end
@@ -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 Agent
6
+ # This class extends RequestContexts: this class acts to encapsulate information about the currently
7
+ # executed request, making it available to the Agent for the duration of the request in a standardized
8
+ # and normalized format which the Agent understands.
9
+ module RequestContextExtend
10
+ BUILD_ATTACK_LOGGER_MESSAGE = 'Building attack result from Contrast Service input analysis result'
11
+ # Convert the discovered route for this request to appropriate forms and disseminate it to those locations
12
+ # where it is necessary for our route coverage and finding vulnerability discovery features to function.
13
+ #
14
+ # @param route [Contrast::Api::Dtm::RouteCoverage, nil] the route of the current request, as determined from the
15
+ # framework
16
+ def append_route_coverage route
17
+ return unless route
18
+
19
+ # For our findings
20
+ @route = route
21
+
22
+ # For SR findings
23
+ @activity.routes << route
24
+
25
+ # For TS routes
26
+ @observed_route.signature = route.route
27
+ @observed_route.verb = route.verb
28
+ @observed_route.url = route.url if route.url
29
+ @request.route = route
30
+ @request.observed_route = @observed_route
31
+ end
32
+
33
+ # Collect the results for the given rule with the given action
34
+ #
35
+ # @param rule [String] the id of the rule to which the results apply
36
+ # @param response_type [Symbol] the result of the response, matching a value of
37
+ # Contrast::Api::Dtm::AttackResult::ResponseType
38
+ # @return [Array<Contrast::Api::Dtm::AttackResult>]
39
+ def results_for rule, response_type = nil
40
+ if response_type.nil?
41
+ activity.results.select { |r| r.rule_id == rule }
42
+ else
43
+ activity.results.select { |r| r.rule_id == rule && r.response == response_type }
44
+ end
45
+ end
46
+
47
+ def service_extract_request
48
+ return false unless ::Contrast::AGENT.enabled?
49
+ return false unless ::Contrast::PROTECT.enabled?
50
+ return false if @do_not_track
51
+
52
+ service_response = Contrast::Agent.messaging_queue.send_event_immediately(@activity.http_request)
53
+ return false unless service_response
54
+
55
+ handle_protect_state(service_response)
56
+ ia = service_response.input_analysis
57
+ if ia
58
+ if logger.trace?
59
+ logger.trace('Analysis from Contrast Service', evaluations: ia.results.length)
60
+ logger.trace('Results', input_analysis: ia.inspect)
61
+ end
62
+ @speedracer_input_analysis = ia
63
+ speedracer_input_analysis.request = request
64
+ else
65
+ logger.trace('Analysis from Contrast Service was empty.')
66
+ false
67
+ end
68
+ rescue Contrast::SecurityException => e
69
+ raise e
70
+ rescue StandardError => e
71
+ logger.warn('Unable to extract Contrast Service information from request', e)
72
+ false
73
+ end
74
+
75
+ # NOTE: this method is only used as a backstop if Speedracer sends Input Evaluations when the protect state
76
+ # indicates a security exception should be thrown. This method ensures that the attack reports are generated.
77
+ # Normally these should be generated on Speedracer for any attacks detected during prefilter.
78
+ #
79
+ # @param agent_settings [Contrast::Api::Settings::AgentSettings]
80
+ def handle_protect_state agent_settings
81
+ return unless agent_settings&.protect_state
82
+
83
+ state = agent_settings.protect_state
84
+ @uuid = state.uuid
85
+ @do_not_track = true unless state.track_request
86
+ return unless state.security_exception
87
+
88
+ # If Contrast Service has NOT handled the input analysis, handle them here
89
+ build_attack_results(agent_settings)
90
+ logger.debug('Contrast Service said to block this request')
91
+ raise Contrast::SecurityException.new(nil, (state.security_message || 'Blocking suspicious behavior'))
92
+ end
93
+
94
+ # append anything we've learned to the request seen message this is the sum-total of all inventory information
95
+ # that has been accumulated since the last request
96
+ def extract_after rack_response
97
+ @response = Contrast::Agent::Response.new(rack_response)
98
+ activity.http_response = @response.dtm if @sample_res
99
+ rescue StandardError => e
100
+ logger.error('Unable to extract information after request', e)
101
+ end
102
+
103
+ private
104
+
105
+ # Generate attack results directly from any evaluations on the agent settings object.
106
+ #
107
+ # @param agent_settings [Contrast::Api::Settings::AgentSettings]
108
+ def build_attack_results agent_settings
109
+ return unless agent_settings&.input_analysis&.results&.any?
110
+
111
+ results_by_rule = {}
112
+ agent_settings.input_analysis.results.each do |ia_result|
113
+ rule_id = ia_result.rule_id
114
+ rule = ::Contrast::PROTECT.rule(rule_id)
115
+ next unless rule
116
+
117
+ logger.debug(BUILD_ATTACK_LOGGER_MESSAGE, result: ia_result.inspect) if logger.debug?
118
+ results_by_rule[rule_id] = attack_result rule, rule_id, ia_result, results_by_rule
119
+ end
120
+
121
+ results_by_rule.each_pair do |_, attack_result|
122
+ logger.info('Blocking attack result', rule: attack_result.rule_id)
123
+ activity.results << attack_result
124
+ end
125
+ end
126
+
127
+ def attack_result rule, rule_id, ia_result, results_by_rule
128
+ @_attack_result = if rule.mode == :BLOCK
129
+ # special case for rules (like reflected xss) that used to have an infilter / block mode
130
+ # but now are just block at perimeter
131
+ rule.build_attack_with_match(self, ia_result, results_by_rule[rule_id], ia_result.value)
132
+ else
133
+ rule.build_attack_without_match(self, ia_result, results_by_rule[rule_id])
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -8,6 +8,7 @@ require 'contrast/utils/object_share'
8
8
  require 'contrast/utils/string_utils'
9
9
  require 'contrast/utils/hash_digest'
10
10
  require 'contrast/components/logger'
11
+ require 'contrast/utils/response_utils'
11
12
 
12
13
  module Contrast
13
14
  module Agent
@@ -17,6 +18,7 @@ module Contrast
17
18
  # order to avoid repeatedly creating Strings & thrashing GC.
18
19
  class Response
19
20
  include Contrast::Components::Logger::InstanceMethods
21
+ include Contrast::Utils::ResponseUtils
20
22
 
21
23
  extend Forwardable
22
24
 
@@ -88,79 +90,6 @@ module Contrast
88
90
  body_content = @is_array ? rack_response[2] : rack_response.body
89
91
  extract_body(body_content)
90
92
  end
91
-
92
- private
93
-
94
- # From the dtm for normalized_response_headers:
95
- # Key is UPPERCASE_UNDERSCORE
96
- #
97
- # Example: Content-Type: text/html; charset=utf-8
98
- # "CONTENT_TYPE" => Content-Type,["text/html; charset=utf8"]
99
- def append_pair map, key, value
100
- return unless key && value
101
- return if value.is_a?(Hash)
102
-
103
- safe_key = Contrast::Utils::StringUtils.force_utf8(key)
104
- hash_key = Contrast::Utils::StringUtils.normalized_key(safe_key)
105
- map[hash_key] ||= Contrast::Api::Dtm::Pair.new
106
- map[hash_key].key = safe_key
107
- map[hash_key].values << Contrast::Utils::StringUtils.force_utf8(value)
108
- end
109
-
110
- HTTP_PREFIX = /^[Hh][Tt][Tt][Pp][_-]/i.cs__freeze
111
-
112
- # Given some holder of the content of the response's body, extract that
113
- # content and return it as a String
114
- #
115
- # @param body [String, Rack::File, Rack::BodyProxy,
116
- # ActionDispatch::Response::RackBody, Rack::Response] Something that
117
- # holds, wraps, or is the body of the Response
118
- # @return [nil, String] the content of the body
119
- def extract_body body
120
- return unless body
121
-
122
- if defined?(Rack::File) && body.is_a?(Rack::File)
123
- # not sure what to do in this situation, so don't do anything.
124
- nil
125
- elsif body.is_a?(Rack::BodyProxy)
126
- handle_rack_body_proxy(body)
127
- elsif (defined?(ActionDispatch::Response::RackBody) && body.is_a?(ActionDispatch::Response::RackBody)) ||
128
- body.is_a?(Rack::Response)
129
-
130
- extract_body(body.body)
131
- elsif Contrast::Utils::DuckUtils.quacks_to?(body, :each)
132
- acc = []
133
- body.each { |tmp| acc << read_or_string(tmp) }
134
- acc.compact.join(Contrast::Utils::ObjectShare::NEW_LINE)
135
- elsif ActionView::OutputBuffer
136
- # https://stackoverflow.com/questions/15654676/how-to-convert-activesupportsafebuffer-to-string
137
- body.to_str
138
- else
139
- read_or_string(body)
140
- end
141
- end
142
-
143
- def handle_rack_body_proxy body
144
- next_body = body.instance_variable_get(:@body)
145
- case next_body
146
- when Array
147
- extract_body(next_body[0])
148
- else
149
- extract_body(next_body)
150
- end
151
- end
152
-
153
- def read_or_string obj
154
- return unless obj
155
-
156
- if Contrast::Utils::DuckUtils.quacks_to?(obj, :read)
157
- tmp = obj.read
158
- obj.rewind
159
- tmp
160
- else
161
- obj.to_s
162
- end
163
- end
164
93
  end
165
94
  end
166
95
  end
@@ -17,10 +17,32 @@ module Contrast
17
17
  include Contrast::Utils::OS
18
18
 
19
19
  APP_AND_SERVER_DATA = ::Contrast::APP_CONTEXT.app_and_server_information.cs__freeze
20
- SAAS_DEFAULT = { addr: 'app.contrastsecuirty.com', type: 'SAAS_DEFAULT' }.cs__freeze
20
+ # Multi-tenant Production Environments
21
+ SAAS_DEFAULT = { addr: 'app.contrastsecurity.com', type: 'SAAS_DEFAULT' }.cs__freeze
22
+ SAAS_CS = { addr: /cs[[:digit:]]+\.contrastsecurity\.com/, type: 'SAAS_DEFAULT' }.cs__freeze
23
+ SAAS_JP = { addr: 'app.contrastsecurity.jp', type: 'SAAS_DEFAULT' }.cs__freeze
21
24
  SAAS_CE = { addr: 'ce.contrastsecurity.com', type: 'SAAS_CE' }.cs__freeze
22
- SAAS_CUSTOM = { addr: 'contrastsecurite.com', type: 'SAAS_CUSTOM' }.cs__freeze
23
- SAAS_POV = { addr: 'eval.contrastsecuirty.com', type: 'SAAS_POV' }.cs__freeze
25
+ # Multi-tenant Demo Environments
26
+ SAAS_DEMO = { addr: 'apptwo.contrastsecurity.com', type: 'SAAS_DEMO' }.cs__freeze
27
+ SAAS_POV = { addr: 'eval.contrastsecurity.com', type: 'SAAS_POV' }.cs__freeze
28
+ # Multi-tenant Testing Environment
29
+ SAAS_RESEARCH = { addr: 'security-research.contrastsecurity.com', type: 'SAAS_RESEARCH' }.cs__freeze
30
+ SAAS_ALPHA = { addr: 'alpha.contrastsecurity.com', type: 'SAAS_ALPHA' }.cs__freeze
31
+ SAAS_STAGING = { addr: 'teamserver-staging.contsec.com', type: 'SAAS_TESTING' }.cs__freeze
32
+ SAAS_STAGING_TOKYO = { addr: 'teamserver-staging.contsec.jp', type: 'SAAS_TESTING' }.cs__freeze
33
+ SAAS_TESTING = { addr: 'teamserver-darpa.contsec.com', type: 'SAAS_TESTING' }.cs__freeze
34
+ SAAS_OPS_TESTING = { addr: 'teamserver-ops.contsec.com', type: 'SAAS_TESTING' }.cs__freeze
35
+ # Fallback for Single-tenant Production Environments
36
+ SAAS_CUSTOM = { addr: 'contrastsecurity.com', type: 'SAAS_CUSTOM' }.cs__freeze
37
+ SAAS_CUSTOM_JP = { addr: 'contrastsecurity.jp', type: 'SAAS_CUSTOM' }.cs__freeze
38
+
39
+ SINGLE_MAP_TENANTS = [
40
+ SAAS_DEFAULT, SAAS_JP, SAAS_CE, SAAS_DEMO, SAAS_POV, SAAS_RESEARCH, SAAS_ALPHA,
41
+ SAAS_STAGING, SAAS_STAGING_TOKYO, SAAS_TESTING, SAAS_OPS_TESTING
42
+ ].cs__freeze
43
+ REGEXP_MAP_TENANTS = [SAAS_CS].cs__freeze
44
+ FALLBACK_TENANTS = [SAAS_CUSTOM, SAAS_CUSTOM_JP].cs__freeze
45
+ # Fallback for Custom, most likely self-hosted, Environments
24
46
  EOP = 'EOP'
25
47
 
26
48
  def initialize
@@ -50,21 +72,22 @@ module Contrast
50
72
 
51
73
  private
52
74
 
53
- # Here we extract the Teamserver url type
75
+ # Here we extract the TeamServer url type
54
76
  #
55
- # @return[String] type, it could be SAAS_DEFAULT, SAAS_POV, SAAS_CE, SAAS_CUSTOM, or EOP
77
+ # @return [String] the type of TeamServer environment to which we're connecting
56
78
  def teamserver_type
57
- @_teamserver_type ||= if Contrast::API.api_url.include?(SAAS_DEFAULT[:addr])
58
- SAAS_DEFAULT[:type]
59
- elsif Contrast::API.api_url.include?(SAAS_POV[:addr])
60
- SAAS_POV[:type]
61
- elsif Contrast::API.api_url.include?(SAAS_CE[:addr])
62
- SAAS_CE[:type]
63
- elsif Contrast::API.api_url.end_with? SAAS_CUSTOM[:addr]
64
- SAAS_CUSTOM[:type]
65
- else
66
- EOP
67
- end
79
+ @_teamserver_type ||= begin
80
+ url = Contrast::API.api_url
81
+ if (single = SINGLE_MAP_TENANTS.find { |tenant| url.include?(tenant[:addr]) })
82
+ single[:type]
83
+ elsif (regexp = REGEXP_MAP_TENANTS.find { |tenant| tenant[:addr].match?(url) })
84
+ regexp[:type]
85
+ elsif (fallback = FALLBACK_TENANTS.find { |tenant| url.include?(tenant[:addr]) })
86
+ fallback[:type]
87
+ else
88
+ EOP
89
+ end
90
+ end
68
91
  end
69
92
  end
70
93
  end
@@ -10,8 +10,8 @@ module Contrast
10
10
  # this module handles one time static analysis tasks
11
11
  class StaticAnalysis
12
12
  include Singleton
13
- include Contrast::Components::Logger::InstanceMethods
14
13
  include Contrast::Components::Scope::InstanceMethods
14
+ extend Contrast::Components::Logger::InstanceMethods
15
15
 
16
16
  class << self
17
17
  # After the first request is complete, we do a one-time manual catchup to review and report the already-loaded
@@ -3,7 +3,7 @@
3
3
 
4
4
  require 'contrast/config/env_variables'
5
5
  require 'contrast/components/logger'
6
- require 'contrast/utils/requests_client'
6
+ require 'contrast/utils/telemetry_client'
7
7
  require 'contrast/agent/worker_thread'
8
8
  require 'contrast/utils/telemetry'
9
9
 
@@ -11,7 +11,6 @@ module Contrast
11
11
  module Agent
12
12
  # This class will initialize and hold everything needed for the telemetry
13
13
  class Telemetry < WorkerThread
14
- include Contrast::Utils::RequestsClient
15
14
  include Contrast::Components::Logger::InstanceMethods
16
15
  # this is where we will send the data from the agents
17
16
  URL = 'https://telemetry.ruby.contrastsecurity.com/'
@@ -28,13 +27,13 @@ module Contrast
28
27
  mac = Contrast::Utils::Telemetry::Identifier.mac
29
28
  app_name = Contrast::Utils::Telemetry::Identifier.app_name
30
29
  id = mac + app_name if mac && app_name
31
- Digest::SHA2.new(256).hexdigest(id || '_' + SecureRandom.uuid)
30
+ Digest::SHA2.new(256).hexdigest(id || ('_' + SecureRandom.uuid))
32
31
  end
33
32
  end
34
33
 
35
34
  def instance_id
36
35
  @_instance_id ||= Digest::SHA2.new(256).hexdigest(Contrast::Utils::Telemetry::Identifier.mac ||
37
- '_' + SecureRandom.uuid)
36
+ ('_' + SecureRandom.uuid))
38
37
  end
39
38
 
40
39
  def enabled?
@@ -51,7 +50,8 @@ module Contrast
51
50
 
52
51
  # In case of connection error, do not create the background thread or queue,
53
52
  # as if the opt-out env var was set
54
- ip_opt_out_telemetry = Contrast::Utils::RequestsClient.initialize_connection(URL)
53
+ @_client = Contrast::Utils::TelemetryClient.new
54
+ ip_opt_out_telemetry = @_client.initialize_connection(URL)
55
55
  if ip_opt_out_telemetry.nil?
56
56
  logger.warn('Connection was not established properly!!!')
57
57
  logger.warn('THE SERVICE IS GOING TO BE TERMINATED!!')
@@ -62,6 +62,14 @@ module Contrast
62
62
  end
63
63
  end
64
64
 
65
+ def client
66
+ @_client ||= Contrast::Utils::TelemetryClient.new
67
+ end
68
+
69
+ def connection
70
+ @_connection ||= client.initialize_connection(URL)
71
+ end
72
+
65
73
  def attempt_to_start?
66
74
  unless cs__class.enabled?
67
75
  logger.warn('Telemetry service is disabled!')
@@ -83,8 +91,8 @@ module Contrast
83
91
 
84
92
  begin
85
93
  logger.debug('This is the current processed event', event)
86
- res = Contrast::Utils::RequestsClient.send_request event, connection
87
- sleep_time = Contrast::Utils::RequestsClient.handle_response res
94
+ res = client.send_request event, connection
95
+ sleep_time = client.handle_response res
88
96
  sleep(sleep_time) unless sleep_time.nil?
89
97
  rescue StandardError => e
90
98
  logger.error('Could not send message to service from telemetry queue.', e)
@@ -11,23 +11,22 @@ module Contrast
11
11
 
12
12
  attr_reader :timestamp, :tags
13
13
 
14
- # The instance_id comes from the Telemetry Instance
15
14
  def initialize
16
15
  @tags = MetricsHash.new(String)
17
- @timestamp = format_date
18
- @instance_id = Contrast::Agent.telemetry_queue.__id__
16
+ @timestamp = Time.now.iso8601
19
17
  end
20
18
 
21
19
  def path
22
20
  ''
23
21
  end
24
22
 
25
- def to_json **_args
26
- { tags: @tags, timestamp: @timestamp, instance_id: @instance_id }
27
- end
28
-
29
- def format_date
30
- Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
23
+ def to_hash **_args
24
+ {
25
+ tags: @tags,
26
+ timestamp: @timestamp,
27
+ instance: Contrast::Agent::Telemetry.instance_id,
28
+ application: Contrast::Agent::Telemetry.application_id
29
+ }
31
30
  end
32
31
  end
33
32
  end
@@ -4,6 +4,7 @@
4
4
  require 'contrast/components/logger'
5
5
  require 'contrast/agent/service_heartbeat'
6
6
  require 'contrast/api/communication/messaging_queue'
7
+ require 'contrast/agent/reporting/report'
7
8
  require 'contrast/agent/telemetry'
8
9
 
9
10
  module Contrast
@@ -24,21 +25,26 @@ module Contrast
24
25
  @heartbeat = Contrast::Agent::ServiceHeartbeat.new
25
26
  @messaging_queue = Contrast::Api::Communication::MessagingQueue.new
26
27
  @telemetry = Contrast::Agent::Telemetry.new if Contrast::Agent::Telemetry.enabled?
28
+ @reporter = Contrast::Agent::Reporter.new if Contrast::Agent::Reporter.enabled?
27
29
  end
28
30
 
29
31
  def startup!
30
32
  return unless ::Contrast::AGENT.enabled?
31
33
 
32
- telemetry_thread_status = telemetry_thread_init
33
- heartbeat_thread_status = heartbeat_thread_init
34
- messaging_thread_status = messaging_thread_init
34
+ telemetry_status = telemetry_thread_init
35
+ heartbeat_status = heartbeat_thread_init
36
+ messaging_status = messaging_thread_init
37
+ reporter_status = reporter_thread_init
35
38
 
36
39
  logger.debug('ThreadWatcher started threads')
37
40
 
38
- @pids[Process.pid] = messaging_thread_status && heartbeat_thread_status
41
+ @pids[Process.pid] = messaging_status && heartbeat_status
39
42
  return @pids unless Contrast::Agent::Telemetry.enabled?
40
43
 
41
- @pids[Process.pid] = messaging_thread_status && heartbeat_thread_status && telemetry_thread_status
44
+ @pids[Process.pid] = @pids[Process.pid] && telemetry_status
45
+ return @pids unless Contrast::Agent::Reporter.enabled?
46
+
47
+ @pids[Process.pid] = @pids[Process.pid] && reporter_status
42
48
  end
43
49
 
44
50
  def ensure_running?
@@ -53,6 +59,7 @@ module Contrast
53
59
  messaging_queue.stop!
54
60
  heapdump_util.stop!
55
61
  telemetry_queue&.stop!
62
+ reporter_queue&.stop!
56
63
  end
57
64
 
58
65
  def heartbeat_thread_init
@@ -82,11 +89,30 @@ module Contrast
82
89
  messaging_result
83
90
  end
84
91
 
92
+ def reporter_thread_init
93
+ @reporter.start_thread! if @reporter&.attempt_to_start?
94
+ unless @reporter&.running?
95
+ logger.debug('Attempting to start reporter thread')
96
+ @reporter&.start_thread!
97
+ end
98
+ reporter_result = @reporter&.running?
99
+ logger.debug('Reporter thread status', alive: reporter_result)
100
+ reporter_result
101
+ end
102
+
103
+ # @return [Contrast::Agent::Telemetry]
85
104
  def telemetry_queue
86
105
  return if @telemetry.nil?
87
106
 
88
107
  @telemetry
89
108
  end
109
+
110
+ # @return [Contrast::Agent::Reporter]
111
+ def reporter_queue
112
+ return if @reporter.nil?
113
+
114
+ @reporter
115
+ end
90
116
  end
91
117
  end
92
118
  end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '4.13.1'
6
+ VERSION = '4.14.0'
7
7
  end
8
8
  end