contrast-agent 7.1.0 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/ext/extconf_common.rb +88 -14
  3. data/lib/contrast/agent/assess/policy/source_method.rb +13 -4
  4. data/lib/contrast/agent/assess/policy/trigger_method.rb +12 -18
  5. data/lib/contrast/agent/excluder/excluder.rb +64 -31
  6. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  7. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  8. data/lib/contrast/agent/protect/rule/base.rb +9 -7
  9. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +1 -1
  10. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +29 -13
  11. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
  12. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
  13. data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
  14. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +2 -2
  15. data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  19. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  20. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  21. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  22. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  23. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  24. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  25. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  26. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  27. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  28. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +1 -1
  29. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  30. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +1 -1
  31. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  32. data/lib/contrast/agent/protect/rule/utils/filters.rb +6 -6
  33. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  34. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +1 -1
  35. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  36. data/lib/contrast/agent/reporting/client/interface.rb +132 -0
  37. data/lib/contrast/agent/reporting/client/interface_base.rb +27 -0
  38. data/lib/contrast/agent/reporting/connection_status.rb +0 -1
  39. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  40. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +17 -4
  41. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  42. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  43. data/lib/contrast/agent/reporting/reporter.rb +11 -26
  44. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  45. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  46. data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +1 -1
  47. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
  48. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +47 -6
  49. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +41 -32
  50. data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +144 -0
  51. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +35 -13
  52. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_mode.rb +14 -1
  53. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +11 -11
  54. data/lib/contrast/agent/request/request.rb +27 -12
  55. data/lib/contrast/agent/telemetry/base.rb +44 -19
  56. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  57. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  58. data/lib/contrast/agent/telemetry/client.rb +10 -2
  59. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +97 -0
  60. data/lib/contrast/agent/telemetry/exception.rb +1 -0
  61. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  62. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  63. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  64. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  65. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  66. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  67. data/lib/contrast/agent/version.rb +1 -1
  68. data/lib/contrast/components/config/sources.rb +6 -5
  69. data/lib/contrast/components/config.rb +4 -4
  70. data/lib/contrast/components/protect.rb +11 -1
  71. data/lib/contrast/components/sampling.rb +15 -10
  72. data/lib/contrast/components/settings.rb +9 -0
  73. data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
  74. data/lib/contrast/config/diagnostics/source_config_value.rb +5 -1
  75. data/lib/contrast/config/diagnostics/tools.rb +4 -4
  76. data/lib/contrast/config/validate.rb +2 -2
  77. data/lib/contrast/config/yaml_file.rb +8 -0
  78. data/lib/contrast/configuration.rb +11 -19
  79. data/lib/contrast/framework/grape/support.rb +1 -2
  80. data/lib/contrast/framework/manager.rb +17 -8
  81. data/lib/contrast/framework/rack/support.rb +99 -1
  82. data/lib/contrast/framework/rails/support.rb +4 -2
  83. data/lib/contrast/framework/sinatra/support.rb +1 -2
  84. data/lib/contrast/logger/aliased_logging.rb +18 -9
  85. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  86. data/lib/contrast/utils/hash_utils.rb +21 -2
  87. data/lib/contrast/utils/metrics_hash.rb +1 -1
  88. data/lib/contrast/utils/object_share.rb +2 -1
  89. data/lib/contrast/utils/request_utils.rb +14 -0
  90. data/lib/contrast/utils/response_utils.rb +12 -0
  91. data/lib/contrast/utils/timer.rb +2 -0
  92. data/lib/contrast.rb +9 -2
  93. data/resources/assess/policy.json +11 -0
  94. data/ruby-agent.gemspec +1 -1
  95. metadata +25 -7
  96. data/lib/contrast/agent/reporting/input_analysis/details/bot_blocker_details.rb +0 -27
  97. data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -6,6 +6,7 @@ require 'contrast/agent/reporting/report'
6
6
  require 'contrast/components/logger'
7
7
  require 'contrast/agent/reporting/reporting_events/agent_startup'
8
8
  require 'contrast/agent/telemetry/exception'
9
+ require 'contrast/agent/reporting/client/interface'
9
10
 
10
11
  module Contrast
11
12
  module Agent
@@ -29,11 +30,7 @@ module Contrast
29
30
  end
30
31
 
31
32
  def client
32
- @_client ||= Contrast::Agent::Reporting::ReporterClient.new
33
- end
34
-
35
- def connection
36
- @_connection ||= client.initialize_connection
33
+ @_client ||= Contrast::Agent::Reporting::Client::Interface.new
37
34
  end
38
35
 
39
36
  def start_thread!
@@ -41,13 +38,12 @@ module Contrast
41
38
  return if running?
42
39
 
43
40
  @connection_attempts = 0
44
-
45
- client.startup!(connection)
46
41
  @_thread = Contrast::Agent::Thread.new do
47
42
  logger.debug('[Reporter] Starting background Reporter thread.')
43
+ client.startup
48
44
  loop do
49
- next unless connected?
50
45
  break unless attempt_to_start?
46
+ next unless connected?
51
47
 
52
48
  process_event(queue.pop)
53
49
  rescue StandardError => e
@@ -61,12 +57,7 @@ module Contrast
61
57
  #
62
58
  # @param event [Contrast::Agent::Reporting::ReportingEvent] Freshly pop-ed event.
63
59
  def handle_resend event
64
- sleep(client.timeout) if client.sleep?
65
- # Retry once than discard the event. This is trigger on too many events of
66
- # same kind error.
67
- client.send_event(event, connection) if client.mode.status == client.mode.resending
68
- client.mode.reset_mode
69
- client.wake_up
60
+ client.handle_resend(event)
70
61
  end
71
62
 
72
63
  # @param event [Contrast::Agent::Reporting::ReportingEvent]
@@ -99,8 +90,9 @@ module Contrast
99
90
  return
100
91
  end
101
92
  return unless event
93
+ return unless connected?
102
94
 
103
- client.send_event(event, connection)
95
+ client.send_event(event)
104
96
  rescue StandardError => e
105
97
  logger.error('[Reporter] Could not send message to TeamServer from reporting queue.', e)
106
98
  end
@@ -124,17 +116,9 @@ module Contrast
124
116
  @_queue ||= Queue.new
125
117
  end
126
118
 
127
- # TODO: RUBY-99999
128
- # The client and connection are being used in multiple threads/ concurrently, and that's not okay. We need
129
- # to figure out why that is and lock it so that it isn't.
130
- #
131
119
  # @return [Boolean]
132
120
  def connected?
133
- if client && connection
134
- # Try to resend startup messages now with connection:
135
- client.startup!(connection) unless client.status.startup_messages_sent?
136
- return true
137
- end
121
+ return true if client.connected?
138
122
 
139
123
  logger.debug('[Reporter] No client/connection; sleeping...')
140
124
  @connection_attempts += 1
@@ -148,8 +132,8 @@ module Contrast
148
132
 
149
133
  # @param event [Contrast::Agent::Reporting::ReportingEvent]
150
134
  def process_event event
151
- client.send_event(event, connection)
152
- handle_resend(event) if client.mode.status == client.mode.resending
135
+ client.send_event(event)
136
+ handle_resend(event) if client.resending?
153
137
  rescue StandardError => e
154
138
  logger.error('[Reporter] Could not send message to TeamServer from reporting queue.', e)
155
139
  end
@@ -171,6 +155,7 @@ module Contrast
171
155
  stack_trace[1].path.delete_prefix(Dir.pwd)
172
156
  end
173
157
  stack_frame_function = stack_trace.nil? || stack_trace[1].nil? ? 'none' : stack_trace[1].label
158
+ stack_frame_type = Contrast::Agent::Telemetry::Exception::Obfuscate.obfuscate_path(stack_frame_type)
174
159
  Contrast::Agent::Telemetry::Exception::StackFrame.build(stack_frame_function, stack_frame_type, nil)
175
160
  end
176
161
  end
@@ -40,6 +40,7 @@ module Contrast
40
40
  # @param attack_result [Contrast::Agent::Reporting::AttackResult]
41
41
  def attach_data attack_result
42
42
  return unless attack_result&.cs__is_a?(Contrast::Agent::Reporting::AttackResult)
43
+ return if attack_result&.empty?
43
44
 
44
45
  attacker_activity = Contrast::Agent::Reporting::ApplicationDefendAttackerActivity.new(ia_request: @request)
45
46
  attacker_activity.attach_data(attack_result)
@@ -5,6 +5,7 @@ require 'contrast/components/logger'
5
5
  require 'contrast/utils/object_share'
6
6
  require 'contrast/utils/duck_utils'
7
7
  require 'contrast/agent/reporting/reporting_events/reportable_hash'
8
+ require 'contrast/agent/reporting/attack_result/response_type'
8
9
  require 'contrast/agent/reporting/reporting_events/application_defend_attack_activity'
9
10
 
10
11
  module Contrast
@@ -81,7 +81,7 @@ module Contrast
81
81
  safe_url = source_or_string(url || pattern)
82
82
 
83
83
  msg = new
84
- msg.signature = "#{ controller }##{ method } #{ safe_pattern }"
84
+ msg.signature = "#{ controller }##{ method } #{ safe_pattern }" # rubocop:disable [Security/Object/Method]
85
85
  msg.verb = Contrast::Utils::StringUtils.force_utf8(method)
86
86
  msg.url = Contrast::Utils::StringUtils.force_utf8(safe_url)
87
87
  msg
@@ -55,17 +55,24 @@ module Contrast
55
55
  # @param event_name [String] the type portion of the file to which to write
56
56
  # @param data [any] The data to be written to the file
57
57
  def write_to_file type, event_name, data = nil
58
- time = Time.now.to_i
58
+ time = Contrast::Utils::Timer.now_ms
59
59
  destination = type == :request ? path_for_requests : path_for_responses
60
60
  # If the feature is disabled or we have yet to create the directory structure, then we could have a nil
61
61
  # destination. In that case, take no action
62
62
  return unless destination
63
63
 
64
- filename = "#{ time }_#{ Thread.current.object_id }_#{ event_name.gsub('::', '-') }_teamserver_#{ type }.json"
64
+ # Get process id and thread id to make sure we don't overwrite files
65
+ process_id = Process.pid
66
+ thread_id = Thread.current.object_id
67
+ event_title = event_name.gsub('::', '-')
68
+ filename = "#{ time }_#{ thread_id }_#{ process_id }_#{ event_title }_teamserver_#{ type }.json"
65
69
  filepath = File.join(destination, filename)
66
70
  logger.debug('Writing to file', eventname: event_name, filename: filename, filepath: filepath)
67
71
  # Here is use append mode, because of a slightly possibility of overwriting an existing file
68
- File.open(filepath, 'a') { |f| f.write(Contrast::Utils::StringUtils.force_utf8(data)) }
72
+ File.open(filepath, 'a') do |f|
73
+ f.write(Contrast::Utils::StringUtils.force_utf8(data))
74
+ f.close
75
+ end
69
76
  rescue StandardError => e
70
77
  logger.warn('Saving audit failed', e: e)
71
78
  end
@@ -14,6 +14,7 @@ require 'contrast/agent/reporting/reporting_utilities/response_handler'
14
14
  require 'contrast/agent/reporting/reporting_events/application_settings'
15
15
  require 'contrast/agent/reporting/reporting_events/agent_effective_config'
16
16
  require 'contrast/agent/reporting/reporting_utilities/reporter_client_utils'
17
+ require 'contrast/agent/reporting/reporting_utilities/resend'
17
18
 
18
19
  module Contrast
19
20
  module Agent
@@ -72,17 +73,17 @@ module Contrast
72
73
  def send_event event, connection
73
74
  return unless connection
74
75
 
76
+ response = nil
75
77
  logger.debug('[Reporter] Preparing to send reporting event', event_class: event.cs__class)
76
-
77
78
  request = build_request(event)
78
79
  response = connection.request(request)
79
- audit&.audit_event(event, response) if ::Contrast::API.request_audit_enable
80
+ audit.audit_event(event, response) if ::Contrast::API.request_audit_enable
80
81
  process_settings_response(response, event)
81
- report_configuration(response, event)
82
82
  process_preflight_response(event, response, connection)
83
+ report_configuration(response, event)
83
84
  response
84
85
  rescue StandardError => e
85
- handle_error(event, e)
86
+ handle_response_error(event, connection, e)
86
87
  end
87
88
 
88
89
  # Write effective config to file:
@@ -103,6 +104,9 @@ module Contrast
103
104
  diagnostics.write_to_file
104
105
  config_event = Contrast::Agent::Reporting::AgentEffectiveConfig.new(diagnostics)
105
106
  Contrast::Agent.reporter.send_event(config_event)
107
+ rescue StandardError => e
108
+ # Don't let this error bubble up and stop the agent reporting, with resending triggered.
109
+ logger.error('[Reporter] Error while reporting configuration', error: e, event_type: event&.cs__class)
106
110
  end
107
111
 
108
112
  def status
@@ -117,10 +121,47 @@ module Contrast
117
121
  @_diagnostics ||= Contrast::Config::Diagnostics::Monitor.new(Contrast::LOGGER.path)
118
122
  end
119
123
 
124
+ # Compress data with Zlib
125
+ #
126
+ # @param event [Contrast::Agent::Reporting::ReportingEvent]
127
+ # @param level [Integer] compression level
128
+ # @param strategy [Integer] compression strategy
129
+ # @return [String] compressed data
130
+ def compress_event event, level = Zlib::DEFAULT_COMPRESSION, strategy = Zlib::DEFAULT_STRATEGY
131
+ compressed_data = StringIO.new.set_encoding(STRING_ENCODING)
132
+ gzip = Zlib::GzipWriter.new(compressed_data, level, strategy)
133
+ gzip.write(event.event_json)
134
+ gzip.close
135
+ gzip = nil
136
+ compressed_event = compressed_data.string.dup
137
+ compressed_data.close
138
+ compressed_data = nil
139
+ compressed_event
140
+ ensure
141
+ gzip&.close
142
+ compressed_data&.close
143
+ compressed_event
144
+ end
145
+
146
+ # Reads compressed data
147
+ #
148
+ # @param compresed_data [String]
149
+ def decompress compresed_data
150
+ Zlib::GzipReader.wrap(StringIO.new(compresed_data), &:read)
151
+ end
152
+
153
+ ##############
154
+ # Forwarders #
155
+ ##############
156
+
120
157
  def sleep?
121
158
  response_handler.sleep?
122
159
  end
123
160
 
161
+ def put_to_sleep
162
+ response_handler.put_to_sleep
163
+ end
164
+
124
165
  def timeout
125
166
  response_handler.timeout
126
167
  end
@@ -129,8 +170,8 @@ module Contrast
129
170
  response_handler.mode
130
171
  end
131
172
 
132
- def reset_mode
133
- response_handler.reset_mode
173
+ def enter_run_mode
174
+ response_handler.enter_run_mode
134
175
  end
135
176
 
136
177
  def wake_up
@@ -8,16 +8,20 @@ require 'contrast/components/scope'
8
8
  require 'contrast/agent/reporting/reporting_events/application_startup'
9
9
  require 'contrast/agent/reporting/reporting_utilities/reporter_client'
10
10
  require 'contrast/agent/reporting/reporting_utilities/endpoints'
11
+ require 'contrast/agent/reporting/reporting_utilities/resend'
11
12
 
12
13
  module Contrast
13
14
  module Agent
14
15
  module Reporting
15
16
  # This module holds utilities required by the reporting service client
16
17
  module ReporterClientUtils
18
+ include Contrast::Agent::Reporting::Resend
17
19
  include Contrast::Components::Logger::InstanceMethods
18
20
  include Contrast::Components::Scope::InstanceMethods
19
21
  include Contrast::Agent::Reporting::Endpoints
20
22
 
23
+ STRING_ENCODING = 'BINARY'
24
+
21
25
  # List the events that need to be sent when agent starts up.
22
26
  # The order here matters -- DO NOT CHANGE IT!!! >_< - HM
23
27
  #
@@ -33,19 +37,37 @@ module Contrast
33
37
 
34
38
  private
35
39
 
36
- # Send Agent Startup event
40
+ # Send Agent Startup event. If error occurs, it will try to resend the message.
37
41
  #
38
42
  # @param connection [Net::HTTP] open connection
39
43
  def send_agent_startup connection
40
- logger.debug('Preparing to send startup messages')
44
+ logger.debug('[Reporter] Preparing to send startup messages')
45
+ STARTUP_EVENTS.each { |event| send_event(event.new, connection) }
46
+ logger.debug('[Reporter] Startup messages sent.') if status.startup_messages_sent?
47
+ end
41
48
 
42
- STARTUP_EVENTS.each do |event|
43
- startup_event = event.new
44
- send_event(startup_event, connection)
45
- rescue StandardError => e
46
- handle_error(startup_event, e)
47
- end
48
- logger.debug('Startup messages sent')
49
+ # Disable reporting and log the error
50
+ #
51
+ # @param event [Contrast::Agent::Reporting::ReportingEvent]
52
+ # @param error [StandardError]
53
+ def disable_reporting event, error
54
+ status.failure!
55
+ mode.resend.reset_rescue_attempts
56
+ mode.status = mode.disabled
57
+ message = '[Reporter] Unable to send message.'
58
+ response_handler.stop_reporting(message,
59
+ application: Contrast::APP_CONTEXT.name, # rubocop:disable Security/Module/Name
60
+ connection_error: error,
61
+ client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME,
62
+ event_id: event&.__id__,
63
+ event_type: event&.cs__class&.cs__name)
64
+ nil
65
+ end
66
+
67
+ def response_success!
68
+ status.success!
69
+ mode.enter_run_mode
70
+ mode.resend.reset_rescue_attempts
49
71
  end
50
72
 
51
73
  # This method will build headers of the request required for TS communication
@@ -65,29 +87,18 @@ module Contrast
65
87
  request
66
88
  end
67
89
 
68
- # Handles standard error case, logs and set status for failure
69
- #
70
- # @param event [Contrast::Agent::Reporting::ReportingEvent]
71
- # One of the DTMs valid for the event field of Contrast::Api::Dtm::Message
72
- # @param error_msg [StandardError]
73
- # @return nil [NilClass] to be passed as response
74
- def handle_error event, error_msg
75
- status.failure!
76
- logger.error('Unable to send message.',
77
- error_msg,
78
- client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME,
79
- event_id: event&.__id__,
80
- event_type: event&.cs__class&.cs__name)
81
- nil
82
- end
83
-
84
90
  # Handles response processing and sets status
85
91
  #
86
92
  # @param event [Contrast::Agent::Reporting::ReportingEvent] The event sent to TeamServer.
87
93
  # @param response [Net::HTTP::Response]
88
94
  def process_settings_response response, event
89
95
  res = response_handler.process(response, event)
90
- status.success! if res
96
+ if res
97
+ status.success!
98
+ mode.resend.reset_rescue_attempts
99
+ else
100
+ status.failure!
101
+ end
91
102
  res
92
103
  end
93
104
 
@@ -104,15 +115,16 @@ module Contrast
104
115
  return unless response&.body && connection
105
116
 
106
117
  findings_to_return = response.body.split(',').delete_if { |el| el.include?('*') }
118
+ mode.resend.reset_rescue_attempts
107
119
  findings_to_return.each do |index|
108
120
  preflight_message = event.messages[index.to_i]
109
- corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message.data)
121
+ corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message&.data)
110
122
  next unless corresponding_finding
111
123
 
112
124
  send_event(corresponding_finding, connection)
113
125
  end
114
126
  rescue StandardError => e
115
- logger.error('Unable to handle response', e)
127
+ logger.error('[Reporter] Unable to handle preflight response', e)
116
128
  end
117
129
 
118
130
  # Convert the given event into an appropriate Net::HTTPRequest object, setting the request headers and
@@ -133,10 +145,7 @@ module Contrast
133
145
  end
134
146
  build_headers(request)
135
147
  event.attach_headers(request)
136
- # compress:
137
- gzip = Zlib::GzipWriter.new(StringIO.new)
138
- gzip << event.event_json
139
- request.body = gzip.close.string
148
+ request.body = compress_event(event)
140
149
  request
141
150
  end
142
151
  end
@@ -0,0 +1,144 @@
1
+ # Copyright (c) 2023 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
+ module Reporting
7
+ # Module to house the Resend logic, when there is no response from TS.
8
+ module Resend
9
+ # How manny times the client will try to rescue from above error before raising an
10
+ # error to reflect the events.
11
+ #
12
+ RESCUE_ATTEMPTS = 3
13
+ TIMEOUT = 5
14
+ RETRY_ERRORS = [
15
+ Net::OpenTimeout, Net::ReadTimeout, ArgumentError, EOFError, OpenSSL::SSL::SSLError, Resolv::ResolvError,
16
+ Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ESHUTDOWN, Errno::EHOSTDOWN, NameError,
17
+ Errno::EHOSTUNREACH, Errno::EISCONN, Errno::ECONNABORTED, Errno::ENETRESET, Errno::ENETUNREACH, SocketError,
18
+ NameError, NoMethodError, Timeout::Error, RuntimeError
19
+ ].cs__freeze
20
+ RESENDING_MESSAGE = '[Reporter] Resending message...'
21
+ END_RESENDING_MESSAGE = '[Reporter] Reporter tried to resend event and received error:'
22
+
23
+ # This method is mainly used in inside error handling and startup mesasges sending.
24
+ #
25
+ # @param event [Contrast::Agent::Reporting::ReportingEvent] event to resend
26
+ # @param connection [Net::HTTP] open connection
27
+ # @param error [StandardError] error that was raised
28
+ # @return [Net::HTTPResponse, nil] response from TS
29
+ def try_resend event, connection, error
30
+ # The timeout for agent to sleep is set here, we don't need to raise an error,
31
+ # but also 15 min is a long time to wait for example if event is startup message.
32
+ # Override timeout
33
+ if Contrast::Agent::Reporting::ReporterClientUtils::STARTUP_EVENTS.include?(event.cs__class)
34
+ response_handler.suspend_reporting(RESENDING_MESSAGE,
35
+ TIMEOUT,
36
+ error,
37
+ event: event,
38
+ log_error: false,
39
+ startup: true)
40
+ end
41
+ mode.enter_resend_mode
42
+ handle_resend(event, connection)
43
+ end
44
+
45
+ # This method will handle the error and will decide if we need to resend the event
46
+ #
47
+ # @param event [Contrast::Agent::Reporting::ReportingEvent]
48
+ # @param connection [Net::HTTP] open connection
49
+ # @return [Net::HTTPResponse, nil] response from TS
50
+ def handle_resend event, connection
51
+ if sleep?
52
+ logger.debug("[Reporter] sleeping for #{ TIMEOUT } seconds before resending...")
53
+ Thread.current.send(:sleep, timeout)
54
+ wake_up
55
+ end
56
+ response = mode.status == mode.resending ? send_event(event, connection) : nil
57
+ response_success! if response
58
+ response
59
+ end
60
+
61
+ # Handles errors that occurs before the ResponseHandler do not have a response, or response
62
+ # code to hande. This Errors requires different handling, because most of errors here
63
+ # occurs when the response cannot be read, there is open ssl error, or a certain Timeout
64
+ # is reached.
65
+ #
66
+ # @param event [Contrast::Agent::Reporting::ReportingEvent]
67
+ # One of the DTMs valid for the event field of Contrast::Api::Dtm::Message
68
+ # @param error [StandardError]
69
+ # @param connection [Net::HTTP] open connection
70
+ # @return nil [NilClass] to be passed as response
71
+ def handle_response_error event, connection, error
72
+ return end_of_rescue(event, error) if mode.resend.rescue_attempts >= RESCUE_ATTEMPTS
73
+
74
+ if RETRY_ERRORS.include?(error.cs__class)
75
+ mode.resend.increase_rescue_attempts
76
+ try_resend(event, connection, error)
77
+ else
78
+ end_of_rescue(event, error)
79
+ end
80
+ end
81
+
82
+ # End of rescue attempts.
83
+ #
84
+ # @param event [Contrast::Agent::Reporting::ReportingEvent]
85
+ # @param error [StandardError]
86
+ # @return [NilClass]
87
+ def end_of_rescue event, error
88
+ if Contrast::Agent::Reporting::ReporterClientUtils::STARTUP_EVENTS.include?(event.cs__class)
89
+ # Agent didn't send it's startup events, There will be reporting errors, disable reporting.
90
+ disable_reporting(event, error)
91
+ else
92
+ # There is an error in one of the reported messages, log that error and continue.
93
+ logger.error(END_RESENDING_MESSAGE,
94
+ resend_attempts: Contrast::Agent::Reporting::Resend::RESCUE_ATTEMPTS,
95
+ connection_error: error,
96
+ client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME,
97
+ event_id: event&.__id__,
98
+ event_type: event&.cs__class&.cs__name)
99
+ end
100
+ end
101
+
102
+ # Disable reporting when there is an error that cannot be handled.
103
+ #
104
+ # @param event [Contrast::Agent::Reporting::ReportingEvent]
105
+ # @param error [StandardError]
106
+ # @return [NilClass]
107
+ def disable_reporting event, error
108
+ logger.error('[Agent] Disabling Reporting...', error: error.message, event_type: event.cs__class&.cs__name)
109
+ Contrast::AGENT.disable!
110
+ end
111
+
112
+ # Methods that will initialize the resending state.
113
+ class Status
114
+ # RESEND_STARTUP_EVENTS = 4
115
+
116
+ def initialize
117
+ @_rescue_attempts = 0
118
+ end
119
+
120
+ # return how many times the client has tried to rescue from error.
121
+ #
122
+ # @return [Integer]
123
+ def rescue_attempts
124
+ @_rescue_attempts ||= 0
125
+ end
126
+
127
+ # Reset Error rescues attempts.
128
+ #
129
+ # @return [Integer]
130
+ def reset_rescue_attempts
131
+ @_rescue_attempts = 0
132
+ end
133
+
134
+ # Increase the Error rescues attempts.
135
+ #
136
+ # @return [Integer]
137
+ def increase_rescue_attempts
138
+ @_rescue_attempts += 1
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -40,7 +40,7 @@ module Contrast
40
40
  assess_on: ::Contrast::ASSESS.enabled?)
41
41
  response
42
42
  rescue StandardError => e
43
- logger.error('Unable to process response from TeamServer', e)
43
+ logger.error('[Reporter] Unable to process response from TeamServer', e)
44
44
  nil
45
45
  end
46
46
 
@@ -63,11 +63,45 @@ module Contrast
63
63
  @_mode ||= Contrast::Agent::Reporting::ResponseHandlerMode.new
64
64
  end
65
65
 
66
+ # Puts the reporting service to sleep
67
+ def put_to_sleep
68
+ @_sleep = true
69
+ end
70
+
66
71
  # Wakes the reporting service
67
72
  def wake_up
68
73
  @_sleep = false
69
74
  end
70
75
 
76
+ # Suspend the reporter and try again in time. If not set
77
+ # the timeout is set to 15 min As default:
78
+ #
79
+ # @param message [String] Message to log.
80
+ # @param timeout [Integer,nil] The timeout to wait and retry after.
81
+ # @param error_message [String, nil] Error message if any received.
82
+ # @param event [Contrast::Agent::Reporting::ReportingEvent, nil] The event sent to TeamServer if set
83
+ # @param log_error [Boolean] Whether to log the error or not.
84
+ # @param startup [Boolean] Whether this is a startup error or not.
85
+ def suspend_reporting message, timeout, error_message, event: nil, log_error: true, startup: false
86
+ @_timeout = timeout || Contrast::Agent::Reporting::ResponseHandler::TIMEOUT
87
+ if log_error
88
+ log_error_msg(message,
89
+ timeout: @_timeout,
90
+ error_message: error_message || 'none',
91
+ event_id: event.nil? ? 'none' : event&.__id__,
92
+ event_type: event.nil? ? 'none' : event&.cs__class&.cs__name)
93
+ end
94
+ if startup
95
+ logger.info(message,
96
+ connection_error: error_message,
97
+ client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME,
98
+ event_id: event&.__id__,
99
+ timeout: timeout,
100
+ event_type: event&.cs__class&.cs__name)
101
+ end
102
+ put_to_sleep
103
+ end
104
+
71
105
  private
72
106
 
73
107
  # Handles the errors code received from TS and takes appropriate action.
@@ -93,18 +127,6 @@ module Contrast
93
127
  end
94
128
  end
95
129
 
96
- # Suspend the reporter and try again in time. If not set
97
- # the timeout is set to 15 min As default:
98
- #
99
- # @param message [String] Message to log.
100
- # @param timeout [Integer,nil] The timeout to wait and retry after.
101
- # @param error_message [String, nil] Error message if any received.
102
- def suspend_reporting message, timeout, error_message
103
- @_timeout = timeout || Contrast::Agent::Reporting::ResponseHandler::TIMEOUT
104
- log_error_msg(message, timeout: @_timeout, error_message: error_message || 'none')
105
- @_sleep = true
106
- end
107
-
108
130
  # Log what we've received.
109
131
  #
110
132
  # @param message [String] Message to log
@@ -1,6 +1,8 @@
1
1
  # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'contrast/agent/reporting/reporting_utilities/resend'
5
+
4
6
  module Contrast
5
7
  module Agent
6
8
  module Reporting
@@ -44,6 +46,17 @@ module Contrast
44
46
  @_status ||= running
45
47
  end
46
48
 
49
+ # Resend Status tracker
50
+ #
51
+ # @return [Contrast::Agent::Reporting::Resend::Status]
52
+ def resend
53
+ @_resend ||= Contrast::Agent::Reporting::Resend::Status.new
54
+ end
55
+
56
+ def enter_resend_mode
57
+ @_status = resending
58
+ end
59
+
47
60
  # Set current mode.
48
61
  #
49
62
  # @return [Symbol] Reporter's client and Response Handler
@@ -54,7 +67,7 @@ module Contrast
54
67
 
55
68
  # Reset mode
56
69
  #
57
- def reset_mode
70
+ def enter_run_mode
58
71
  @_status = running
59
72
  end
60
73
  end