contrast-agent 4.3.2 → 4.4.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent.rb +5 -1
  3. data/lib/contrast/agent/assess.rb +0 -9
  4. data/lib/contrast/agent/assess/contrast_event.rb +0 -2
  5. data/lib/contrast/agent/assess/contrast_object.rb +5 -2
  6. data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
  7. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
  8. data/lib/contrast/agent/assess/policy/propagation_method.rb +28 -13
  9. data/lib/contrast/agent/assess/policy/propagator/append.rb +28 -13
  10. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -16
  11. data/lib/contrast/agent/assess/policy/propagator/splat.rb +23 -13
  12. data/lib/contrast/agent/assess/policy/propagator/split.rb +14 -7
  13. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +30 -14
  14. data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
  15. data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
  16. data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
  17. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -2
  18. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
  19. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
  20. data/lib/contrast/agent/assess/properties.rb +0 -2
  21. data/lib/contrast/agent/assess/property/tagged.rb +37 -19
  22. data/lib/contrast/agent/assess/tracker.rb +1 -1
  23. data/lib/contrast/agent/middleware.rb +85 -55
  24. data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
  25. data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
  26. data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
  27. data/lib/contrast/agent/protect/rule/sqli.rb +17 -11
  28. data/lib/contrast/agent/request_context.rb +12 -0
  29. data/lib/contrast/agent/thread.rb +1 -1
  30. data/lib/contrast/agent/thread_watcher.rb +20 -5
  31. data/lib/contrast/agent/version.rb +1 -1
  32. data/lib/contrast/api/communication/messaging_queue.rb +18 -21
  33. data/lib/contrast/api/communication/response_processor.rb +8 -1
  34. data/lib/contrast/api/communication/socket_client.rb +22 -14
  35. data/lib/contrast/api/decorators.rb +2 -0
  36. data/lib/contrast/api/decorators/agent_startup.rb +58 -0
  37. data/lib/contrast/api/decorators/application_startup.rb +51 -0
  38. data/lib/contrast/api/decorators/route_coverage.rb +15 -5
  39. data/lib/contrast/api/decorators/trace_event.rb +42 -14
  40. data/lib/contrast/components/agent.rb +2 -0
  41. data/lib/contrast/components/app_context.rb +4 -22
  42. data/lib/contrast/components/sampling.rb +48 -6
  43. data/lib/contrast/components/settings.rb +5 -4
  44. data/lib/contrast/framework/manager.rb +13 -12
  45. data/lib/contrast/framework/rails/support.rb +42 -43
  46. data/lib/contrast/framework/sinatra/support.rb +100 -41
  47. data/lib/contrast/logger/log.rb +31 -15
  48. data/lib/contrast/utils/class_util.rb +3 -1
  49. data/lib/contrast/utils/heap_dump_util.rb +103 -87
  50. data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
  51. data/resources/assess/policy.json +3 -9
  52. data/resources/deadzone/policy.json +6 -0
  53. data/ruby-agent.gemspec +54 -16
  54. metadata +105 -136
  55. data/lib/contrast/agent/assess/rule.rb +0 -18
  56. data/lib/contrast/agent/assess/rule/base.rb +0 -52
  57. data/lib/contrast/agent/assess/rule/redos.rb +0 -67
  58. data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
  59. data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
  60. data/lib/contrast/utils/prevent_serialization.rb +0 -52
@@ -13,7 +13,7 @@ module Contrast
13
13
  # one does not exist.
14
14
  #
15
15
  # @param mod [Module] the Module for which the status is asked
16
- # @return [Contrast::Agent::Patching::Policy::PolicyStatus]
16
+ # @return [Contrast::Agent::Patching::Policy::PatchStatus]
17
17
  def get_status mod
18
18
  if mod.cs__const_defined?(status_key, false)
19
19
  mod.cs__const_get(status_key, false)
@@ -52,7 +52,7 @@ module Contrast
52
52
  # startup to catchup on everything we didn't see get loaded
53
53
  def patch
54
54
  catchup_after_load_patches
55
- patch_methods
55
+ catchup_loaded_methods
56
56
  Contrast::Agent::Assess::Policy::RewriterPatch.rewrite_interpolations
57
57
  end
58
58
 
@@ -62,10 +62,11 @@ module Contrast
62
62
  # where only a subset of the Assess changes are needed.
63
63
  PATCH_MONITOR = Monitor.new
64
64
 
65
- def patch_methods
65
+ # Iterate over and patch those Modules and Methods which were loaded before the Agent was enabled.
66
+ def catchup_loaded_methods
66
67
  PATCH_MONITOR.synchronize do
67
68
  t = Contrast::Agent::Thread.new do
68
- synchronized_patch_methods
69
+ synchronized_catchup_loaded_methods
69
70
  end
70
71
  # aborting on exception makes exceptions propagate.
71
72
  t.abort_on_exception = true
@@ -100,7 +101,7 @@ module Contrast
100
101
  # @param mod [Module] the module in which the patch should be
101
102
  # placed.
102
103
  # @param methods [Array(Symbol)] all the instance or singleton
103
- # methods in this clazz.
104
+ # methods in this mod.
104
105
  # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
105
106
  # the policy that applies to the given method_name.
106
107
  # @return [Boolean] if patched, either by this invocation or a
@@ -149,7 +150,7 @@ module Contrast
149
150
  # other functions, like rewriting or scanning. This method should
150
151
  # only be invoked by the patch_methods method above in order to
151
152
  # ensure it is wrapped in a synchronize call
152
- def synchronized_patch_methods
153
+ def synchronized_catchup_loaded_methods
153
154
  logger.trace_with_time('Running patching') do
154
155
  patched = []
155
156
  all_module_names.each do |patchable_name|
@@ -166,53 +167,37 @@ module Contrast
166
167
  end
167
168
  end
168
169
 
169
- # Given the patchers that apply to this class that may apply, patch
170
- # Contrast method calls into the methods for which we have rules.
170
+ # Given the patchers that apply to this class that may apply, patch Contrast method calls into the methods
171
+ # for which we have rules.
171
172
  #
172
- # @param module_data [Contrast::Agent::ModuleData] the module, and
173
- # its name, that's being patched into
174
- # @param redo_patch [Boolean] a trigger to force patching
175
- # regardless of the state of the
176
- # Contrast::Agent::Patching::Policy::PatchStatus status on the
177
- # Module
173
+ # @param module_data [Contrast::Agent::ModuleData] the module, and its name, that's being patched into
174
+ # @param redo_patch [Boolean] a trigger to force patching regardless of the state of the
175
+ # Contrast::Agent::Patching::Policy::PatchStatus status on the Module
178
176
  def patch_into_module module_data, redo_patch = false
179
177
  status = status_type.get_status(module_data.mod)
180
178
  return if (status&.patched? || status&.patching?) && !redo_patch
181
179
 
182
- # Begin patching our sources into the given clazz (or module)
183
- # Any patcher that has the name of the clazz will be evaluated for
184
- # patching.
185
- # Find all the patchers that apply to this class, sorted by type.
180
+ # Begin patching our sources into the given module. Any patcher that has the name of the module will be
181
+ # evaluated for patching. Find all the patchers that apply to this class, sorted by type.
186
182
  module_policy = Contrast::Agent::Patching::Policy::ModulePolicy.create_module_policy(module_data.name)
187
-
188
- clazz = module_data.mod
183
+ # If there's nothing to match, then set that status and exit
184
+ if module_policy.empty?
185
+ status.no_patch!
186
+ return
187
+ end
189
188
 
190
189
  status.patching!
191
- patched = false
192
-
193
- counts = 0
194
- # Monkey patch any methods in this class that have matching nodes in the policy
195
- unless module_policy.empty?
196
- instance_methods = all_instance_methods(clazz, true)
197
- singleton_methods = clazz.singleton_methods(false)
198
- counts += patch_into_methods(clazz, instance_methods, module_policy, true)
199
- counts += patch_into_methods(clazz, singleton_methods, module_policy, false)
200
- counts = module_policy.num_expected_patches if adjust_for_prepend(clazz)
201
- patched = true
202
- end
190
+ num_applied_patches = patch_into_instance_methods(module_data, module_policy)
191
+ num_applied_patches += patch_into_singleton_methods(module_data, module_policy)
192
+ if adjust_for_prepend(module_data) ||
193
+ module_policy.num_expected_patches == num_applied_patches
203
194
 
204
- if patched
205
- if module_policy.num_expected_patches == counts
206
- status.patched!
207
- else
208
- status.partial_patch!
209
- end
195
+ status.patched!
210
196
  else
211
- status.no_patch!
197
+ status.partial_patch!
212
198
  end
213
199
  rescue StandardError => e
214
- status ||= status_type.get_status(module_data.mod)
215
- status.failed_patch!
200
+ status&.failed_patch!
216
201
  logger.warn('Patching failed', e, module: module_data.name)
217
202
  ensure
218
203
  logger.trace('Patching complete',
@@ -249,6 +234,29 @@ module Contrast
249
234
  instance_methods
250
235
  end
251
236
 
237
+ # Patch into the Instance Methods, including private, of the given Module that match the ModulePolicy
238
+ # provided.
239
+ #
240
+ # @param module_data [Contrast::Agent::ModuleData] the module, and its name, that's being patched into
241
+ # @param module_policy [Contrast::Agent::Patching::Policy::ModulePolicy] All the patchers that apply to
242
+ # this module, sorted by type.
243
+ def patch_into_instance_methods module_data, module_policy
244
+ mod = module_data.mod
245
+ methods = all_instance_methods(mod, true)
246
+ patch_into_methods(mod, methods, module_policy, true)
247
+ end
248
+
249
+ # Patch into the Singleton Methods of the given Module that match the ModulePolicy provided.
250
+ #
251
+ # @param module_data [Contrast::Agent::ModuleData] the module, and its name, that's being patched into
252
+ # @param module_policy [Contrast::Agent::Patching::Policy::ModulePolicy] All the patchers that apply to
253
+ # this module, sorted by type.
254
+ def patch_into_singleton_methods module_data, module_policy
255
+ mod = module_data.mod
256
+ methods = mod.singleton_methods(false)
257
+ patch_into_methods(mod, methods, module_policy, false)
258
+ end
259
+
252
260
  # We've found the patchers that apply to this class (or module). Now we'll
253
261
  # filter on the given method.
254
262
  #
@@ -279,11 +287,10 @@ module Contrast
279
287
  # it has to be reapplied.
280
288
  # TODO: RUBY-620 should remove the need for this
281
289
  #
282
- # @param mod[Module] the Module for which a prepend action needs to
283
- # be accounted
290
+ # @param module_data [Contrast::Agent::ModuleData] the module, and its name, that's being patched into
284
291
  # @return [Boolean] if an adjustment was made or not
285
- def adjust_for_prepend mod
286
- return false unless mod.cs__name == 'CGI::Util'
292
+ def adjust_for_prepend module_data
293
+ return false unless module_data.mod.cs__name == 'CGI::Util'
287
294
 
288
295
  CGI.include(CGI::Util)
289
296
  CGI.extend(CGI::Util)
@@ -46,6 +46,11 @@ module Contrast
46
46
  raise(ArgumentError,
47
47
  "#{ id } did not have a proper applicator method: #{ applicator } does not respond to #{ applicator_method }. Unable to create.")
48
48
  end
49
+ validate_properties
50
+ validate_rule
51
+ end
52
+
53
+ def validate_properties
49
54
  if (required_properties & optional_properties).any?
50
55
  raise(ArgumentError, "#{ rule_id } had overlapping elements between required and optional properties. Unable to create.")
51
56
  end
@@ -53,8 +58,6 @@ module Contrast
53
58
  raise(ArgumentError, "#{ id } had an unexpected property. Unable to create.")
54
59
  end
55
60
  raise(ArgumentError, "#{ id } did not have a required property. Unable to create.") if (required_properties - properties.keys).any?
56
-
57
- validate_rule
58
61
  end
59
62
 
60
63
  def validate_rule
@@ -50,16 +50,9 @@ module Contrast
50
50
  last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
51
51
  next unless last_boundary && boundary
52
52
 
53
- input_analysis_result.attack_count = input_analysis_result.attack_count + 1
54
-
55
- kwargs[:start_idx] = idx
56
- kwargs[:end_idx] = idx + length
57
- kwargs[:boundary_overrun_idx] = boundary
58
- kwargs[:input_boundary_idx] = last_boundary
59
-
60
53
  result ||= build_attack_result(context)
61
- update_successful_attack_response(context, input_analysis_result, result, query_string)
62
- append_sample(context, input_analysis_result, result, query_string, **kwargs)
54
+ record_match(idx, length, boundary, last_boundary, kwargs)
55
+ append_match(context, input_analysis_result, result, query_string, kwargs)
63
56
  end
64
57
 
65
58
  result
@@ -75,13 +68,26 @@ module Contrast
75
68
  sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
76
69
  sample.sqli.start_idx = sample.sqli.query.index(input).to_i
77
70
  sample.sqli.end_idx = sample.sqli.start_idx + input.length
78
- sample.sqli.boundary_overrun_idx = kwargs[:boundary].to_i
79
- sample.sqli.input_boundary_idx = kwargs[:last_boundary].to_i
71
+ sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
72
+ sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
80
73
  sample
81
74
  end
82
75
 
83
76
  private
84
77
 
78
+ def record_match idx, length, boundary, last_boundary, kwargs
79
+ kwargs[:start_idx] = idx
80
+ kwargs[:end_idx] = idx + length
81
+ kwargs[:boundary_overrun_idx] = boundary
82
+ kwargs[:input_boundary_idx] = last_boundary
83
+ end
84
+
85
+ def append_match context, input_analysis_result, result, query_string, **kwargs
86
+ input_analysis_result.attack_count = input_analysis_result.attack_count + 1
87
+ update_successful_attack_response(context, input_analysis_result, result, query_string)
88
+ append_sample(context, input_analysis_result, result, query_string, **kwargs)
89
+ end
90
+
85
91
  def select_scanner database
86
92
  @sql_scanners ||= {
87
93
  Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
@@ -12,6 +12,18 @@ module Contrast
12
12
  # This class acts to encapsulate information about the currently executed
13
13
  # request, making it available to the Agent for the duration of the request
14
14
  # in a standardized and normalized format which the Agent understands.
15
+ #
16
+ # @attr_reader timer [Contrast::Utils::Timer] when the context was created
17
+ # @attr_reader logging_hash [Hash] context used to log the request
18
+ # @attr_reader speedracer_input_analysis [Contrast::Api::Settings::InputAnalysis] the protect input analysis of
19
+ # sources on this request
20
+ # @attr_reader request [Contrast::Agent::Request] our wrapper around the Rack::Request for this context
21
+ # @attr_reader response [Contrast::Agent::Response] our wrapper aroudn the Rack::Response or Array for this context,
22
+ # only available after the application has finished its processing
23
+ # @attr_reader activity [Contrast::Api::Dtm::Activity] the application activity found in this request
24
+ # @attr_reader server_activity [Contrast::Api::Dtm::ServerActivity] the server activity found in this request
25
+ # @attr_reader route [Contrast::Api::Dtm::RouteCoverage] the route, used for findings, of this request
26
+ # @attr_reader observed_route [Contrast::Api::Dtm::ObservedRoute] the route, used for coverage, of this request
15
27
  class RequestContext
16
28
  include Contrast::Components::Interface
17
29
  access_component :agent, :analysis, :logging, :scope
@@ -5,7 +5,7 @@ require 'contrast/components/interface'
5
5
 
6
6
  module Contrast
7
7
  module Agent
8
- # Threads used by Contrast.
8
+ # Threads used by Contrast. Any long running thread should be created and managed by our ThreadWatcher class.
9
9
  class Thread < ::Thread
10
10
  include Contrast::Components::Interface
11
11
 
@@ -3,22 +3,31 @@
3
3
 
4
4
  require 'contrast/components/interface'
5
5
  require 'contrast/agent/service_heartbeat'
6
+ require 'contrast/api/communication/messaging_queue'
6
7
 
7
8
  module Contrast
8
9
  module Agent
9
10
  # This class used to ensure that our worker threads are running in multi-process environments
11
+ #
12
+ # @attr_reader heapdump_util [Contrast::Utils::HeapDumpUtil]
13
+ # @attr_reader heartbeat [Contrast::Agent::ServiceHeartbeat]
14
+ # @attr_reader messaging_queue [Contrast::Api::Communication::MessagingQueue]
10
15
  class ThreadWatcher
11
16
  include Contrast::Components::Interface
12
- access_component :logging
17
+ access_component :agent, :logging
13
18
 
14
- attr_reader :heartbeat
19
+ attr_reader :heapdump_util, :heartbeat, :messaging_queue
15
20
 
16
21
  def initialize
17
22
  @pids = {}
23
+ @heapdump_util = Contrast::Utils::HeapDumpUtil.new
18
24
  @heartbeat = Contrast::Agent::ServiceHeartbeat.new
25
+ @messaging_queue = Contrast::Api::Communication::MessagingQueue.new
19
26
  end
20
27
 
21
28
  def startup!
29
+ return unless AGENT.enabled?
30
+
22
31
  unless heartbeat.running?
23
32
  logger.debug('Attempting to start heartbeat thread')
24
33
  heartbeat.start_thread!
@@ -26,11 +35,11 @@ module Contrast
26
35
  heartbeat_result = heartbeat.running?
27
36
  logger.debug('Heartbeat thread status', alive: heartbeat_result)
28
37
 
29
- unless Contrast::Agent.messaging_queue.running?
38
+ unless messaging_queue.running?
30
39
  logger.debug('Attempting to start messaging queue thread')
31
- Contrast::Agent.messaging_queue.start_thread!
40
+ messaging_queue.start_thread!
32
41
  end
33
- messaging_result = Contrast::Agent.messaging_queue.running?
42
+ messaging_result = messaging_queue.running?
34
43
  logger.debug('Messaging thread status', alive: messaging_result)
35
44
 
36
45
  logger.debug('ThreadWatcher started threads')
@@ -44,6 +53,12 @@ module Contrast
44
53
  logger.debug('ThreadWatcher - threads not running')
45
54
  startup!
46
55
  end
56
+
57
+ def shutdown!
58
+ heartbeat.stop!
59
+ messaging_queue.stop!
60
+ heapdump_util.stop!
61
+ end
47
62
  end
48
63
  end
49
64
  end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '4.3.2'
6
+ VERSION = '4.4.0'
7
7
  end
8
8
  end
@@ -10,7 +10,7 @@ module Contrast
10
10
  # Top level gateway to messaging with speedracer
11
11
  class MessagingQueue < Contrast::Agent::WorkerThread
12
12
  include Contrast::Components::Interface
13
- access_component :analysis, :logging, :settings
13
+ access_component :agent, :analysis, :logging, :settings
14
14
 
15
15
  attr_reader :queue, :speedracer
16
16
 
@@ -22,11 +22,19 @@ module Contrast
22
22
 
23
23
  # Use this to bypass the messaging queue and leave response processing to the caller
24
24
  def send_event_immediately event
25
- send_event(event, true)
25
+ if AGENT.disabled?
26
+ logger.warn('Attempted to send event immediately with Agent disabled', caller: caller, event: event)
27
+ return
28
+ end
29
+ speedracer.return_response(event)
26
30
  end
27
31
 
28
32
  # Use this to add a message to the queue and process the response internally
29
33
  def send_event_eventually event
34
+ if AGENT.disabled?
35
+ logger.warn('Attempted to queue event with Agent disabled', caller: caller, event: event)
36
+ return
37
+ end
30
38
  logger.debug('Enqueued event for sending', event_type: event.cs__class)
31
39
  queue << event if event
32
40
  end
@@ -35,13 +43,14 @@ module Contrast
35
43
  speedracer.ensure_startup!
36
44
  return if running?
37
45
 
46
+ @queue ||= Queue.new
38
47
  @_thread = Contrast::Agent::Thread.new do
39
48
  loop do
40
49
  event = queue.pop
41
50
 
42
51
  begin
43
52
  logger.debug('Dequeued event for sending', event_type: event.cs__class)
44
- send_event(event)
53
+ speedracer.process_internally(event)
45
54
  rescue StandardError => e
46
55
  logger.error('Could not send message to service from messaging queue thread.', e)
47
56
  end
@@ -50,25 +59,13 @@ module Contrast
50
59
  logger.debug('Started background sending thread.')
51
60
  end
52
61
 
53
- private
62
+ def stop!
63
+ return unless running?
54
64
 
55
- # return_response is used to determine if we want to return the response to the caller or process it internally
56
- def send_event event, return_response = false
57
- preprocess_event(event)
58
- if return_response
59
- speedracer.return_response(event)
60
- else
61
- speedracer.process_internally(event)
62
- end
63
- end
64
-
65
- # For now this only handles appending assess tags
66
- # eventually we could break out preprocessors for every event type
67
- def preprocess_event event
68
- return unless event.is_a?(Contrast::Api::Dtm::Activity)
69
-
70
- # See if they're even enabled
71
- event.findings.delete_if { |finding| ASSESS.rule_disabled?(finding.rule_id) }
65
+ super
66
+ @queue&.clear
67
+ @queue&.close
68
+ @queue = nil
72
69
  end
73
70
  end
74
71
  end
@@ -11,6 +11,7 @@ module Contrast
11
11
  include Contrast::Components::Interface
12
12
  access_component :agent, :analysis, :logging, :settings
13
13
 
14
+ # @param response [Contrast::Api::Settings::AgentSettings]
14
15
  def process response
15
16
  logger.debug('Received a response', sent_ms: response&.sent_ms)
16
17
 
@@ -31,8 +32,10 @@ module Contrast
31
32
 
32
33
  private
33
34
 
34
- # Given some protobuf messages, update settings.
35
+ # Given some protobuf messages, update server features.
35
36
  # This is the bridge between Contrast Service <-> Settings.
37
+ #
38
+ # @param response [Contrast::Api::Settings::AgentSettings]
36
39
  def process_server_response response
37
40
  server_features = response&.server_features
38
41
  return unless server_features
@@ -44,6 +47,10 @@ module Contrast
44
47
  server_features
45
48
  end
46
49
 
50
+ # Given some protobuf messages, update application settings.
51
+ # This is the bridge between Contrast Service <-> Settings.
52
+ #
53
+ # @param response [Contrast::Api::Settings::AgentSettings]
47
54
  def process_application_response response
48
55
  app_settings = response&.application_settings
49
56
  return unless app_settings
@@ -26,6 +26,7 @@ module Contrast
26
26
  #
27
27
  # @param event [Contrast::Api::Dtm] One of the DTMs valid for the event field of
28
28
  # Contrast::Api::Dtm::Message
29
+ # @return [Contrast::Api::Settings::AgentSettings]
29
30
  def send_one event
30
31
  msg = Contrast::Api::Dtm::Message.build(event)
31
32
  send_message(msg)
@@ -58,24 +59,31 @@ module Contrast
58
59
  end
59
60
 
60
61
  # Or something is not set.
61
- msg = if CONFIG.root.agent.service.host
62
- 'Missing a required connection value to the Contrast Service. ' \
63
- '`agent.service.port` is not set. ' \
64
- 'Falling back to default TCP socket port.'
65
- elsif CONFIG.root.agent.service.port
66
- 'Missing a required connection value to the Contrast Service. ' \
67
- '`agent.service.host` is not set. ' \
68
- 'Falling back to default TCP socket host.'
69
- else
70
- 'Missing a required connection value to the Contrast Service. ' \
71
- 'Neither `agent.service.socket` nor the pair of `agent.service.host` and `agent.service.port` are set. '\
72
- 'Falling back to default TCP socket.'
73
- end
74
- logger.warn(msg,
62
+ logger.warn(log_connection_error_msg,
75
63
  host: CONTRAST_SERVICE.host,
76
64
  port: CONTRAST_SERVICE.port)
77
65
  end
78
66
 
67
+ # If our connection isn't built properly, we need to warn the user. This builds out the context specific
68
+ # message to provide that warning
69
+ #
70
+ # @return [String]
71
+ def log_connection_error_msg
72
+ if CONFIG.root.agent.service.host
73
+ 'Missing a required connection value to the Contrast Service. ' \
74
+ '`agent.service.port` is not set. ' \
75
+ 'Falling back to default TCP socket port.'
76
+ elsif CONFIG.root.agent.service.port
77
+ 'Missing a required connection value to the Contrast Service. ' \
78
+ '`agent.service.host` is not set. ' \
79
+ 'Falling back to default TCP socket host.'
80
+ else
81
+ 'Missing a required connection value to the Contrast Service. ' \
82
+ 'Neither `agent.service.socket` nor the pair of `agent.service.host` and `agent.service.port` are set. '\
83
+ 'Falling back to default TCP socket.'
84
+ end
85
+ end
86
+
79
87
  def send_message msg
80
88
  return unless msg
81
89