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.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
- data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
- data/lib/contrast/agent/assess/policy/trigger_method.rb +44 -7
- data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
- data/lib/contrast/agent/assess/property/tagged.rb +51 -57
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
- data/lib/contrast/agent/metric_telemetry_event.rb +2 -2
- data/lib/contrast/agent/middleware.rb +5 -75
- data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
- data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
- data/lib/contrast/agent/patching/policy/patcher.rb +12 -8
- data/lib/contrast/agent/reporting/report.rb +21 -0
- data/lib/contrast/agent/reporting/reporter.rb +142 -0
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
- data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
- data/lib/contrast/agent/request.rb +2 -81
- data/lib/contrast/agent/request_context.rb +4 -128
- data/lib/contrast/agent/request_context_extend.rb +138 -0
- data/lib/contrast/agent/response.rb +2 -73
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +39 -16
- data/lib/contrast/agent/static_analysis.rb +1 -1
- data/lib/contrast/agent/telemetry.rb +15 -7
- data/lib/contrast/agent/telemetry_event.rb +8 -9
- data/lib/contrast/agent/thread_watcher.rb +31 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +15 -0
- data/lib/contrast/api/communication/connection_status.rb +10 -7
- data/lib/contrast/api/communication/messaging_queue.rb +37 -3
- data/lib/contrast/api/communication/response_processor.rb +15 -8
- data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
- data/lib/contrast/api/communication/socket.rb +6 -8
- data/lib/contrast/api/communication/socket_client.rb +29 -12
- data/lib/contrast/api/communication/speedracer.rb +37 -1
- data/lib/contrast/api/communication/tcp_socket.rb +4 -3
- data/lib/contrast/api/communication/unix_socket.rb +1 -0
- data/lib/contrast/api/decorators/finding.rb +45 -0
- data/lib/contrast/components/api.rb +56 -0
- data/lib/contrast/components/app_context.rb +10 -65
- data/lib/contrast/components/app_context_extend.rb +78 -0
- data/lib/contrast/components/base.rb +23 -0
- data/lib/contrast/components/config.rb +8 -8
- data/lib/contrast/components/contrast_service.rb +5 -0
- data/lib/contrast/components/sampling.rb +2 -2
- data/lib/contrast/config/agent_configuration.rb +1 -1
- data/lib/contrast/config/api_configuration.rb +9 -4
- data/lib/contrast/config/api_proxy_configuration.rb +14 -0
- data/lib/contrast/config/application_configuration.rb +2 -3
- data/lib/contrast/config/assess_configuration.rb +3 -3
- data/lib/contrast/config/base_configuration.rb +17 -28
- data/lib/contrast/config/certification_configuration.rb +15 -0
- data/lib/contrast/config/env_variables.rb +2 -9
- data/lib/contrast/config/heap_dump_configuration.rb +6 -6
- data/lib/contrast/config/inventory_configuration.rb +1 -5
- data/lib/contrast/config/protect_rule_configuration.rb +1 -1
- data/lib/contrast/config/request_audit_configuration.rb +18 -0
- data/lib/contrast/config/ruby_configuration.rb +6 -6
- data/lib/contrast/config/service_configuration.rb +1 -2
- data/lib/contrast/config.rb +0 -1
- data/lib/contrast/configuration.rb +1 -2
- data/lib/contrast/extension/assess/array.rb +5 -7
- data/lib/contrast/framework/manager.rb +8 -32
- data/lib/contrast/framework/manager_extend.rb +50 -0
- data/lib/contrast/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/sinatra/support.rb +2 -1
- data/lib/contrast/logger/log.rb +8 -103
- data/lib/contrast/utils/assess/property/tagged_utils.rb +23 -0
- data/lib/contrast/utils/assess/tracking_util.rb +20 -15
- data/lib/contrast/utils/assess/trigger_method_utils.rb +1 -1
- data/lib/contrast/utils/class_util.rb +18 -14
- data/lib/contrast/utils/findings.rb +62 -0
- data/lib/contrast/utils/hash_digest.rb +10 -73
- data/lib/contrast/utils/hash_digest_extend.rb +86 -0
- data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
- data/lib/contrast/utils/heap_dump_util.rb +2 -65
- data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
- data/lib/contrast/utils/io_util.rb +1 -1
- data/lib/contrast/utils/log_utils.rb +108 -0
- data/lib/contrast/utils/middleware_utils.rb +87 -0
- data/lib/contrast/utils/net_http_base.rb +158 -0
- data/lib/contrast/utils/object_share.rb +1 -0
- data/lib/contrast/utils/request_utils.rb +88 -0
- data/lib/contrast/utils/response_utils.rb +97 -0
- data/lib/contrast/utils/substitution_utils.rb +167 -0
- data/lib/contrast/utils/tag_util.rb +9 -9
- data/lib/contrast/utils/telemetry.rb +4 -2
- data/lib/contrast/utils/telemetry_client.rb +90 -0
- data/lib/contrast/utils/telemetry_identifier.rb +17 -24
- data/ruby-agent.gemspec +5 -5
- metadata +48 -23
- data/lib/contrast/config/default_value.rb +0 -17
- 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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
75
|
+
# Here we extract the TeamServer url type
|
|
54
76
|
#
|
|
55
|
-
# @return[String] type
|
|
77
|
+
# @return [String] the type of TeamServer environment to which we're connecting
|
|
56
78
|
def teamserver_type
|
|
57
|
-
@_teamserver_type ||=
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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/
|
|
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
|
-
|
|
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 =
|
|
87
|
-
sleep_time =
|
|
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 =
|
|
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
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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] =
|
|
41
|
+
@pids[Process.pid] = messaging_status && heartbeat_status
|
|
39
42
|
return @pids unless Contrast::Agent::Telemetry.enabled?
|
|
40
43
|
|
|
41
|
-
@pids[Process.pid] =
|
|
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
|