contrast-agent 4.11.0 → 4.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +1 -0
  3. data/ext/cs__assess_module/cs__assess_module.c +48 -0
  4. data/ext/cs__assess_module/cs__assess_module.h +7 -0
  5. data/ext/cs__common/cs__common.c +24 -7
  6. data/ext/cs__common/cs__common.h +12 -2
  7. data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -11
  8. data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -2
  9. data/ext/cs__os_information/cs__os_information.c +31 -0
  10. data/ext/cs__os_information/cs__os_information.h +7 -0
  11. data/ext/{cs__protect_kernel → cs__os_information}/extconf.rb +0 -0
  12. data/lib/contrast/agent/assess/contrast_event.rb +1 -1
  13. data/lib/contrast/agent/assess/contrast_object.rb +1 -1
  14. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
  15. data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
  16. data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
  17. data/lib/contrast/agent/assess/policy/preshift.rb +19 -6
  18. data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
  19. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  20. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
  21. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -0
  22. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
  23. data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
  24. data/lib/contrast/agent/assess/policy/trigger_method.rb +45 -110
  25. data/lib/contrast/agent/assess/policy/trigger_node.rb +62 -21
  26. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
  27. data/lib/contrast/agent/assess/property/tagged.rb +66 -189
  28. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
  29. data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
  30. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +1 -0
  31. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  32. data/lib/contrast/agent/middleware.rb +14 -62
  33. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +0 -1
  34. data/lib/contrast/agent/patching/policy/method_policy.rb +3 -44
  35. data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
  36. data/lib/contrast/agent/patching/policy/patch.rb +37 -238
  37. data/lib/contrast/agent/patching/policy/patcher.rb +15 -50
  38. data/lib/contrast/agent/reporting/report.rb +21 -0
  39. data/lib/contrast/agent/reporting/reporter.rb +142 -0
  40. data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
  41. data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
  42. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
  43. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
  44. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
  45. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
  46. data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
  47. data/lib/contrast/agent/request.rb +2 -81
  48. data/lib/contrast/agent/request_context.rb +18 -126
  49. data/lib/contrast/agent/request_context_extend.rb +138 -0
  50. data/lib/contrast/agent/request_handler.rb +7 -3
  51. data/lib/contrast/agent/response.rb +2 -73
  52. data/lib/contrast/agent/rule_set.rb +2 -4
  53. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +94 -0
  54. data/lib/contrast/agent/static_analysis.rb +5 -3
  55. data/lib/contrast/agent/telemetry.rb +137 -0
  56. data/lib/contrast/agent/telemetry_event.rb +33 -0
  57. data/lib/contrast/agent/thread_watcher.rb +66 -11
  58. data/lib/contrast/agent/version.rb +1 -1
  59. data/lib/contrast/agent.rb +21 -1
  60. data/lib/contrast/api/communication/connection_status.rb +10 -7
  61. data/lib/contrast/api/communication/messaging_queue.rb +37 -3
  62. data/lib/contrast/api/communication/response_processor.rb +15 -8
  63. data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
  64. data/lib/contrast/api/communication/socket.rb +6 -8
  65. data/lib/contrast/api/communication/socket_client.rb +29 -12
  66. data/lib/contrast/api/communication/speedracer.rb +37 -1
  67. data/lib/contrast/api/communication/tcp_socket.rb +4 -3
  68. data/lib/contrast/api/communication/unix_socket.rb +1 -0
  69. data/lib/contrast/api/decorators/finding.rb +45 -0
  70. data/lib/contrast/components/api.rb +90 -0
  71. data/lib/contrast/components/app_context.rb +10 -41
  72. data/lib/contrast/components/app_context_extend.rb +78 -0
  73. data/lib/contrast/components/assess.rb +7 -0
  74. data/lib/contrast/components/base.rb +23 -0
  75. data/lib/contrast/components/config.rb +92 -13
  76. data/lib/contrast/components/contrast_service.rb +11 -0
  77. data/lib/contrast/components/sampling.rb +2 -2
  78. data/lib/contrast/config/agent_configuration.rb +1 -1
  79. data/lib/contrast/config/api_configuration.rb +27 -0
  80. data/lib/contrast/config/api_proxy_configuration.rb +14 -0
  81. data/lib/contrast/config/application_configuration.rb +2 -3
  82. data/lib/contrast/config/assess_configuration.rb +3 -2
  83. data/lib/contrast/config/base_configuration.rb +17 -28
  84. data/lib/contrast/config/certification_configuration.rb +15 -0
  85. data/lib/contrast/config/env_variables.rb +18 -0
  86. data/lib/contrast/config/heap_dump_configuration.rb +6 -6
  87. data/lib/contrast/config/inventory_configuration.rb +1 -5
  88. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  89. data/lib/contrast/config/request_audit_configuration.rb +18 -0
  90. data/lib/contrast/config/root_configuration.rb +1 -0
  91. data/lib/contrast/config/ruby_configuration.rb +6 -6
  92. data/lib/contrast/config/service_configuration.rb +2 -2
  93. data/lib/contrast/config.rb +1 -1
  94. data/lib/contrast/configuration.rb +4 -2
  95. data/lib/contrast/extension/assess/array.rb +5 -7
  96. data/lib/contrast/framework/manager.rb +22 -44
  97. data/lib/contrast/framework/manager_extend.rb +50 -0
  98. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  99. data/lib/contrast/framework/rails/patch/support.rb +31 -29
  100. data/lib/contrast/framework/rails/railtie.rb +1 -1
  101. data/lib/contrast/framework/sinatra/support.rb +2 -1
  102. data/lib/contrast/logger/application.rb +4 -0
  103. data/lib/contrast/logger/log.rb +8 -103
  104. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  105. data/lib/contrast/utils/assess/property/tagged_utils.rb +165 -0
  106. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  107. data/lib/contrast/utils/assess/tracking_util.rb +20 -15
  108. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  109. data/lib/contrast/utils/class_util.rb +65 -54
  110. data/lib/contrast/utils/exclude_key.rb +20 -0
  111. data/lib/contrast/utils/findings.rb +62 -0
  112. data/lib/contrast/utils/hash_digest.rb +10 -73
  113. data/lib/contrast/utils/hash_digest_extend.rb +86 -0
  114. data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
  115. data/lib/contrast/utils/heap_dump_util.rb +2 -65
  116. data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
  117. data/lib/contrast/utils/io_util.rb +1 -1
  118. data/lib/contrast/utils/log_utils.rb +108 -0
  119. data/lib/contrast/utils/lru_cache.rb +4 -2
  120. data/lib/contrast/utils/metrics_hash.rb +59 -0
  121. data/lib/contrast/utils/middleware_utils.rb +87 -0
  122. data/lib/contrast/utils/net_http_base.rb +158 -0
  123. data/lib/contrast/utils/object_share.rb +1 -0
  124. data/lib/contrast/utils/os.rb +23 -0
  125. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  126. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  127. data/lib/contrast/utils/request_utils.rb +88 -0
  128. data/lib/contrast/utils/response_utils.rb +97 -0
  129. data/lib/contrast/utils/substitution_utils.rb +167 -0
  130. data/lib/contrast/utils/tag_util.rb +9 -9
  131. data/lib/contrast/utils/telemetry.rb +79 -0
  132. data/lib/contrast/utils/telemetry_client.rb +90 -0
  133. data/lib/contrast/utils/telemetry_identifier.rb +130 -0
  134. data/lib/contrast.rb +19 -1
  135. data/resources/assess/policy.json +12 -6
  136. data/resources/deadzone/policy.json +86 -5
  137. data/ruby-agent.gemspec +7 -6
  138. data/service_executables/VERSION +1 -1
  139. data/service_executables/linux/contrast-service +0 -0
  140. data/service_executables/mac/contrast-service +0 -0
  141. metadata +68 -26
  142. data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
  143. data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
  144. data/lib/contrast/config/default_value.rb +0 -17
  145. data/lib/contrast/extension/protect/kernel.rb +0 -29
@@ -0,0 +1,66 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/components/logger'
5
+
6
+ module Contrast
7
+ module Agent
8
+ module Reporting
9
+ # This module will be used to store everything that would be sent
10
+ # and depends on the response we receive
11
+ module ReportingStorage
12
+ class << self
13
+ include Contrast::Components::Logger::InstanceMethods
14
+
15
+ # So the collection will be used to store those events
16
+ def collection
17
+ @_collection ||= {}
18
+ end
19
+
20
+ # @param key[String] the key for the pair
21
+ # @param value[Object] the value we need to store
22
+ # @return [Object] the value we saved
23
+ def []= key, value
24
+ return unless key
25
+ return unless value
26
+
27
+ logger.debug('Saving new value', key: key)
28
+
29
+ key = key.to_s.downcase.strip
30
+ collection[key] = value
31
+
32
+ value # rubocop:disable Lint/Void
33
+ end
34
+
35
+ # @param key[String] the key for the pair
36
+ def [] key
37
+ return unless key
38
+
39
+ collection[key]
40
+ end
41
+ alias_method :get, :[]
42
+ alias_method :set, :[]=
43
+
44
+ def delete key
45
+ return unless key
46
+
47
+ logger.debug('Starting deleting value for', key: key)
48
+
49
+ deleted_value = collection.delete(key)
50
+ logger.debug('Key wasn\'t found') unless deleted_value
51
+
52
+ deleted_value
53
+ end
54
+
55
+ # @param rule_id [String] the rule_id
56
+ # @return [Hash, nil] return array with key and value of the pair
57
+ def find_by_rule_id rule_id
58
+ return unless rule_id
59
+
60
+ collection.find { |_, v| v.rule_id == rule_id }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -9,6 +9,7 @@ require 'contrast/utils/string_utils'
9
9
  require 'contrast/utils/hash_digest'
10
10
  require 'contrast/components/logger'
11
11
  require 'contrast/components/scope'
12
+ require 'contrast/utils/request_utils'
12
13
 
13
14
  module Contrast
14
15
  module Agent
@@ -17,6 +18,7 @@ module Contrast
17
18
  # data in a format that the Agent expects, caching those transformations in
18
19
  # order to avoid repeatedly creating Strings & thrashing GC.
19
20
  class Request
21
+ include Contrast::Utils::RequestUtils
20
22
  include Contrast::Components::Logger::InstanceMethods
21
23
  include Contrast::Components::Scope::InstanceMethods
22
24
 
@@ -152,87 +154,6 @@ module Contrast
152
154
 
153
155
  accepts.start_with?(*MEDIA_TYPE_MARKERS)
154
156
  end
155
-
156
- private
157
-
158
- # Return a flattened hash of params with realized paths for keys, in
159
- # addition to a separate, valueless, entry for each nest key.
160
- # See RUBY-621 for more details.
161
- # { key : { nested_key : ['x','y','z' ] } }
162
- # becomes
163
- # {
164
- # key[nested_key][0] : 'x'
165
- # key[nested_key][1] : 'y'
166
- # key[nested_key][2] : 'z'
167
- # key : ''
168
- # nested_key : ''
169
- # }
170
- def normalize_params val, prefix: nil
171
- # In non-recursive invocations, val should always be a Hash
172
- # (rather than breaking this out into two methods)
173
- case val
174
- when Tempfile
175
- # Skip if it's the auto-generated value from rails when it handles
176
- # file uploads. The file name will still be sent to SR for analysis.
177
- {}
178
- when Hash
179
- res = val.each_with_object({}) do |(k, v), hash|
180
- k = Contrast::Utils::StringUtils.force_utf8(k)
181
- nested_prefix = prefix.nil? ? k : "#{ prefix }[#{ k }]"
182
- hash[k] = Contrast::Utils::ObjectShare::EMPTY_STRING
183
- hash.merge! normalize_params(v, prefix: nested_prefix)
184
- end
185
- res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
186
- res
187
- when Enumerable
188
- idx = 0
189
- res = {}
190
- while idx < val.length
191
- res.merge! normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]")
192
- idx += 1
193
- end
194
- res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
195
- res
196
- else
197
- { prefix => Contrast::Utils::StringUtils.force_utf8(val) }
198
- end
199
- end
200
-
201
- def read_body body
202
- return body if body.is_a?(String)
203
-
204
- begin
205
- can_rewind = Contrast::Utils::DuckUtils.quacks_to?(body, :rewind)
206
- # if we are after a middleware that failed to rewind
207
- body.rewind if can_rewind
208
- body.read
209
- rescue StandardError => e
210
- logger.error('Error in attempt to read body', message: e.message)
211
- logger.trace('With Stack', e)
212
- body.to_s
213
- ensure
214
- # be a good citizen and rewind
215
- body.rewind if can_rewind
216
- end
217
- end
218
-
219
- def traverse_parsed_multipart multipart_data, current_names
220
- return current_names unless multipart_data
221
-
222
- multipart_data.each_value do |data_value|
223
- next unless data_value.is_a?(Hash)
224
-
225
- tempfile = data_value[:tempfile]
226
- if tempfile.nil?
227
- traverse_parsed_multipart(data_value, current_names)
228
- else
229
- name = data_value[:name].to_s
230
- file_name = data_value[:filename].to_s
231
- current_names[name] = file_name
232
- end
233
- end
234
- current_names
235
- end
236
157
  end
237
158
  end
238
159
  end
@@ -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
 
@@ -60,14 +64,10 @@ module Contrast
60
64
  # generic holder for properties that can be set throughout this request
61
65
  @_properties = {}
62
66
 
63
- @sample = true
64
-
65
67
  if ::Contrast::ASSESS.enabled?
66
- @sample_request, @sample_response = Contrast::Utils::Assess::SamplingUtil.instance.sample?(@request)
68
+ @sample_req, @sample_res = Contrast::Utils::Assess::SamplingUtil.instance.sample?(@request)
67
69
  end
68
70
 
69
- @sample_response &&= ::Contrast::ASSESS.scan_response?
70
-
71
71
  append_route_coverage(Contrast::Agent.framework_manager.get_route_dtm(@request))
72
72
  end
73
73
  end
@@ -77,103 +77,31 @@ module Contrast
77
77
  end
78
78
 
79
79
  def analyze_request?
80
- @sample_request
80
+ analyze_request_assess? || analyze_req_res_protect?
81
81
  end
82
82
 
83
83
  def analyze_response?
84
- @sample_response
84
+ analyze_response_assess? || analyze_req_res_protect?
85
85
  end
86
86
 
87
- # Convert the discovered route for this request to appropriate forms and disseminate it to those locations
88
- # where it is necessary for our route coverage and finding vulnerability discovery features to function.
89
- #
90
- # @param route [Contrast::Api::Dtm::RouteCoverage, nil] the route of the current request, as determined from the
91
- # framework
92
- def append_route_coverage route
93
- return unless route
94
-
95
- # For our findings
96
- @route = route
97
-
98
- # For SR findings
99
- @activity.routes << route
100
-
101
- # For TS routes
102
- @observed_route.signature = route.route
103
- @observed_route.verb = route.verb
104
- @observed_route.url = route.url if route.url
105
- @request.route = route
106
- @request.observed_route = @observed_route
107
- end
108
-
109
- # Collect the results for the given rule with the given action
110
- #
111
- # @param rule [String] the id of the rule to which the results apply
112
- # @param response_type [Symbol] the result of the response, matching a value of
113
- # Contrast::Api::Dtm::AttackResult::ResponseType
114
- # @return [Array<Contrast::Api::Dtm::AttackResult>]
115
- def results_for rule, response_type = nil
116
- if response_type.nil?
117
- activity.results.select { |r| r.rule_id == rule }
118
- else
119
- activity.results.select { |r| r.rule_id == rule && r.response == response_type }
120
- end
87
+ def analyze_req_res_protect?
88
+ ::Contrast::PROTECT.enabled?
121
89
  end
122
90
 
123
- def service_extract_request
124
- return false unless ::Contrast::AGENT.enabled?
125
- return false unless ::Contrast::PROTECT.enabled?
126
- return false if @do_not_track
91
+ def analyze_request_assess?
92
+ return false unless analyze_req_res_assess?
127
93
 
128
- service_response = Contrast::Agent.messaging_queue.send_event_immediately(@activity.http_request)
129
- return false unless service_response
130
-
131
- handle_protect_state(service_response)
132
- ia = service_response.input_analysis
133
- if ia
134
- if logger.trace?
135
- logger.trace('Analysis from Contrast Service', evaluations: ia.results.length)
136
- logger.trace('Results', input_analysis: ia.inspect)
137
- end
138
- @speedracer_input_analysis = ia
139
- speedracer_input_analysis.request = request
140
- else
141
- logger.trace('Analysis from Contrast Service was empty.')
142
- false
143
- end
144
- rescue Contrast::SecurityException => e
145
- raise e
146
- rescue StandardError => e
147
- logger.warn('Unable to extract Contrast Service information from request', e)
148
- false
94
+ @sample_req
149
95
  end
150
96
 
151
- # NOTE: this method is only used as a backstop if Speedracer sends Input Evaluations when the protect state
152
- # indicates a security exception should be thrown. This method ensures that the attack reports are generated.
153
- # Normally these should be generated on Speedracer for any attacks detected during prefilter.
154
- #
155
- # @param agent_settings [Contrast::Api::Settings::AgentSettings]
156
- def handle_protect_state agent_settings
157
- return unless agent_settings&.protect_state
158
-
159
- state = agent_settings.protect_state
160
- @uuid = state.uuid
161
- @do_not_track = true unless state.track_request
162
- return unless state.security_exception
163
-
164
- # If Contrast Service has NOT handled the input analysis, handle them here
165
- build_attack_results(agent_settings)
166
- logger.debug('Contrast Service said to block this request')
167
- raise Contrast::SecurityException.new(nil, (state.security_message || 'Blocking suspicious behavior'))
97
+ def analyze_response_assess?
98
+ return false unless analyze_req_res_assess?
99
+
100
+ @sample_res &&= ::Contrast::ASSESS.scan_response?
168
101
  end
169
102
 
170
- # append anything we've learned to the request seen message this is the sum-total of all inventory information
171
- # that has been accumulated since the last request
172
- def extract_after rack_response
173
- @response = Contrast::Agent::Response.new(rack_response)
174
- activity.http_response = @response.dtm if @sample_response
175
- rescue StandardError => e
176
- logger.error('Unable to extract information after request', e)
103
+ def analyze_req_res_assess?
104
+ ::Contrast::ASSESS.enabled?
177
105
  end
178
106
 
179
107
  def add_property key, value
@@ -189,42 +117,6 @@ module Contrast
189
117
  @server_activity = Contrast::Api::Dtm::ServerActivity.new
190
118
  @observed_route = Contrast::Api::Dtm::ObservedRoute.new
191
119
  end
192
-
193
- private
194
-
195
- # Generate attack results directly from any evaluations on the agent settings object.
196
- #
197
- # @param agent_settings [Contrast::Api::Settings::AgentSettings]
198
- def build_attack_results agent_settings
199
- return unless agent_settings&.input_analysis&.results&.any?
200
-
201
- attack_results_by_rule = {}
202
- agent_settings.input_analysis.results.each do |ia_result|
203
- rule_id = ia_result.rule_id
204
- rule = ::Contrast::PROTECT.rule(rule_id)
205
- next unless rule
206
-
207
- if logger.debug?
208
- logger.debug('Building attack result from Contrast Service input analysis result',
209
- result: ia_result.inspect)
210
- end
211
-
212
- attack_result = if rule.mode == :BLOCK
213
- # special case for rules (like reflected xss) that used to have an infilter / block mode
214
- # but now are just block at perimeter
215
- rule.build_attack_with_match(self, ia_result, attack_results_by_rule[rule_id],
216
- ia_result.value)
217
- else
218
- rule.build_attack_without_match(self, ia_result, attack_results_by_rule[rule_id])
219
- end
220
- attack_results_by_rule[rule_id] = attack_result
221
- end
222
-
223
- attack_results_by_rule.each_pair do |_, attack_result|
224
- logger.info('Blocking attack result', rule: attack_result.rule_id)
225
- activity.results << attack_result
226
- end
227
- end
228
120
  end
229
121
  end
230
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
@@ -6,19 +6,23 @@ require 'contrast/components/scope'
6
6
 
7
7
  module Contrast
8
8
  module Agent
9
- # This class is instantiated when we receive a request and the agent is enabled to process
10
- # that request. It holds the ruleset that we perform filtering operations on (currently
11
- # prefilter and postfilter).
9
+ # This class is instantiated when we receive a request and the agent is enabled to process that request. It holds
10
+ # the ruleset that we perform filtering operations on (currently prefilter and postfilter).
12
11
  class RequestHandler
13
12
  include Contrast::Components::Logger::InstanceMethods
14
13
 
15
14
  attr_reader :ruleset, :context
16
15
 
16
+ # @param context [Contrast::Agent::RequestContext] the context of the request for which this handler applies
17
17
  def initialize context
18
18
  @context = context
19
19
  @ruleset = ::Contrast::AGENT.ruleset
20
20
  end
21
21
 
22
+ # TODO: RUBY-1353
23
+ # TODO: RUBY-1355
24
+ # TODO: RUBY-1357
25
+ # TODO: RUBY-1358
22
26
  def send_activity_messages
23
27
  Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.generate_library_usage(context.activity)
24
28
  [context.server_activity, context.activity, context.observed_route].each do |message|
@@ -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
@@ -16,8 +16,7 @@ module Contrast
16
16
  # terminate requests on attack detection if set to block at perimeter
17
17
  def prefilter
18
18
  context = Contrast::Agent::REQUEST_TRACKER.current
19
- # TODO: RUBY-801 We shouldn't be responsible for knowing what modes are enabled
20
- return unless context&.analyze_request? || ::Contrast::PROTECT.enabled?
19
+ return unless context&.analyze_request?
21
20
 
22
21
  logger.trace_with_time('Running prefilter...') do
23
22
  map { |rule| rule.prefilter(context) }
@@ -33,8 +32,7 @@ module Contrast
33
32
  # has been created. The main actions here are analyzing the response for unsafe state or actions.
34
33
  def postfilter
35
34
  context = Contrast::Agent::REQUEST_TRACKER.current
36
- # TODO: RUBY-801 We shouldn't be responsible for knowing what modes are enabled
37
- return unless context&.analyze_response? || ::Contrast::PROTECT.enabled?
35
+ return unless context&.analyze_response?
38
36
 
39
37
  logger.trace_with_time('Running postfilter...') do
40
38
  map { |rule| rule.postfilter(context) }