contrast-agent 6.6.3 → 6.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.gitmodules +0 -3
  4. data/ext/cs__scope/cs__scope.c +1 -1
  5. data/lib/contrast/agent/assess/contrast_event.rb +2 -24
  6. data/lib/contrast/agent/assess/events/source_event.rb +7 -61
  7. data/lib/contrast/agent/assess/finalizers/hash.rb +11 -0
  8. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +0 -55
  9. data/lib/contrast/agent/assess/policy/policy_node.rb +3 -3
  10. data/lib/contrast/agent/assess/policy/policy_node_utils.rb +0 -1
  11. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  12. data/lib/contrast/agent/assess/policy/source_method.rb +24 -1
  13. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +7 -5
  14. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +6 -1
  15. data/lib/contrast/agent/assess/policy/trigger_method.rb +38 -119
  16. data/lib/contrast/agent/assess/policy/trigger_node.rb +3 -3
  17. data/lib/contrast/agent/assess/property/evented.rb +2 -12
  18. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +42 -82
  19. data/lib/contrast/agent/assess/rule/response/base_rule.rb +11 -27
  20. data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -3
  21. data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +77 -62
  22. data/lib/contrast/agent/assess/rule/response/csp_header_insecure_rule.rb +1 -1
  23. data/lib/contrast/agent/assess/rule/response/framework/rails_support.rb +6 -1
  24. data/lib/contrast/agent/assess/rule/response/header_rule.rb +5 -5
  25. data/lib/contrast/agent/assess/rule/response/hsts_header_rule.rb +1 -1
  26. data/lib/contrast/agent/assess/rule/response/x_xss_protection_header_rule.rb +1 -1
  27. data/lib/contrast/agent/assess/tracker.rb +1 -7
  28. data/lib/contrast/agent/at_exit_hook.rb +1 -7
  29. data/lib/contrast/agent/excluder.rb +206 -0
  30. data/lib/contrast/agent/exclusion_matcher.rb +6 -0
  31. data/lib/contrast/agent/inventory/database_config.rb +18 -23
  32. data/lib/contrast/agent/middleware.rb +0 -1
  33. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +4 -0
  34. data/lib/contrast/agent/protect/policy/applies_sqli_rule.rb +1 -0
  35. data/lib/contrast/agent/protect/rule/base.rb +64 -24
  36. data/lib/contrast/agent/protect/rule/base_service.rb +1 -0
  37. data/lib/contrast/agent/protect/rule/cmd_injection.rb +18 -104
  38. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +129 -0
  39. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +169 -0
  40. data/lib/contrast/agent/protect/rule/deserialization.rb +7 -5
  41. data/lib/contrast/agent/protect/rule/path_traversal.rb +9 -7
  42. data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +16 -14
  43. data/lib/contrast/agent/protect/rule/sqli/sqli_base_rule.rb +51 -0
  44. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +67 -0
  45. data/lib/contrast/agent/protect/rule/sqli.rb +6 -31
  46. data/lib/contrast/agent/protect/rule/xxe.rb +11 -6
  47. data/lib/contrast/agent/protect/rule.rb +3 -1
  48. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +8 -0
  49. data/lib/contrast/agent/reporting/attack_result/rasp_rule_sample.rb +91 -36
  50. data/lib/contrast/agent/reporting/attack_result/user_input.rb +11 -0
  51. data/lib/contrast/agent/reporting/details/bot_blocker_details.rb +29 -0
  52. data/lib/contrast/agent/reporting/details/cmd_injection_details.rb +30 -0
  53. data/lib/contrast/agent/reporting/details/details.rb +18 -0
  54. data/lib/contrast/agent/reporting/details/http_method_tempering_details.rb +27 -0
  55. data/lib/contrast/agent/reporting/details/ip_denylist_details.rb +27 -0
  56. data/lib/contrast/agent/reporting/details/no_sqli_details.rb +36 -0
  57. data/lib/contrast/agent/reporting/details/path_traversal_details.rb +24 -0
  58. data/lib/contrast/agent/reporting/details/path_traversal_semantic_analysis_details.rb +32 -0
  59. data/lib/contrast/agent/reporting/details/protect_rule_details.rb +17 -0
  60. data/lib/contrast/agent/reporting/details/sqli_dangerous_functions.rb +22 -0
  61. data/lib/contrast/agent/reporting/details/sqli_details.rb +36 -0
  62. data/lib/contrast/agent/reporting/details/untrusted_deserialization_details.rb +27 -0
  63. data/lib/contrast/agent/reporting/details/virtual_patch_details.rb +24 -0
  64. data/lib/contrast/agent/reporting/details/xss_details.rb +33 -0
  65. data/lib/contrast/agent/reporting/details/xss_match.rb +30 -0
  66. data/lib/contrast/agent/reporting/details/xxe_details.rb +36 -0
  67. data/lib/contrast/agent/reporting/details/xxe_match.rb +25 -0
  68. data/lib/contrast/agent/reporting/details/xxe_wrapper.rb +25 -0
  69. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +1 -1
  70. data/lib/contrast/agent/reporting/masker/masker.rb +78 -65
  71. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -30
  72. data/lib/contrast/agent/reporting/reporter.rb +1 -2
  73. data/lib/contrast/agent/reporting/reporting_events/agent_startup.rb +2 -2
  74. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +81 -15
  75. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +13 -25
  76. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_activity.rb +17 -22
  77. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample.rb +46 -125
  78. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample_activity.rb +5 -16
  79. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +10 -18
  80. data/lib/contrast/agent/reporting/reporting_events/application_inventory_activity.rb +6 -14
  81. data/lib/contrast/agent/reporting/reporting_events/application_startup.rb +1 -1
  82. data/lib/contrast/agent/reporting/reporting_events/architecture_component.rb +7 -21
  83. data/lib/contrast/agent/reporting/reporting_events/finding.rb +19 -49
  84. data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +12 -9
  85. data/lib/contrast/agent/reporting/reporting_events/finding_event_signature.rb +1 -1
  86. data/lib/contrast/agent/reporting/reporting_events/finding_event_source.rb +23 -21
  87. data/lib/contrast/agent/reporting/reporting_events/finding_event_stack.rb +5 -18
  88. data/lib/contrast/agent/reporting/reporting_events/finding_event_taint_range.rb +1 -0
  89. data/lib/contrast/{api/decorators/trace_taint_range_tags.rb → agent/reporting/reporting_events/finding_event_taint_range_tags.rb} +7 -6
  90. data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +45 -10
  91. data/lib/contrast/agent/reporting/reporting_events/library_usage_observation.rb +1 -1
  92. data/lib/contrast/agent/reporting/reporting_events/observed_route.rb +2 -2
  93. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +10 -14
  94. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +11 -0
  95. data/lib/contrast/agent/reporting/reporting_events/route_coverage.rb +3 -1
  96. data/lib/contrast/agent/reporting/reporting_events/route_discovery.rb +11 -23
  97. data/lib/contrast/agent/reporting/reporting_events/route_discovery_observation.rb +8 -26
  98. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +1 -1
  99. data/lib/contrast/agent/reporting/reporting_utilities/build_preflight.rb +4 -7
  100. data/lib/contrast/agent/reporting/reporting_utilities/headers.rb +1 -1
  101. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +2 -1
  102. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +3 -3
  103. data/lib/contrast/agent/request.rb +4 -2
  104. data/lib/contrast/agent/request_context.rb +12 -15
  105. data/lib/contrast/agent/request_context_extend.rb +67 -69
  106. data/lib/contrast/agent/request_handler.rb +1 -11
  107. data/lib/contrast/agent/response.rb +0 -18
  108. data/lib/contrast/agent/service_heartbeat.rb +1 -1
  109. data/lib/contrast/agent/telemetry/events/event.rb +1 -1
  110. data/lib/contrast/agent/telemetry/events/metric_event.rb +1 -1
  111. data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +3 -3
  112. data/lib/contrast/agent/version.rb +1 -1
  113. data/lib/contrast/api/communication/messaging_queue.rb +2 -3
  114. data/lib/contrast/api/communication/socket_client.rb +4 -4
  115. data/lib/contrast/api/communication/speedracer.rb +4 -8
  116. data/lib/contrast/api/decorators/agent_startup.rb +5 -6
  117. data/lib/contrast/api/decorators/application_settings.rb +2 -1
  118. data/lib/contrast/api/decorators/application_startup.rb +6 -6
  119. data/lib/contrast/api/decorators/message.rb +0 -4
  120. data/lib/contrast/api/decorators/rasp_rule_sample.rb +0 -6
  121. data/lib/contrast/api/decorators.rb +0 -6
  122. data/lib/contrast/api/dtm.pb.rb +0 -489
  123. data/lib/contrast/components/agent.rb +16 -12
  124. data/lib/contrast/components/api.rb +10 -10
  125. data/lib/contrast/components/app_context.rb +9 -9
  126. data/lib/contrast/components/app_context_extend.rb +1 -1
  127. data/lib/contrast/components/assess.rb +92 -38
  128. data/lib/contrast/components/assess_rules.rb +36 -0
  129. data/lib/contrast/components/config.rb +54 -12
  130. data/lib/contrast/components/contrast_service.rb +8 -8
  131. data/lib/contrast/components/heap_dump.rb +1 -1
  132. data/lib/contrast/components/protect.rb +5 -5
  133. data/lib/contrast/components/ruby_component.rb +81 -0
  134. data/lib/contrast/components/sampling.rb +1 -1
  135. data/lib/contrast/components/security_logger.rb +23 -0
  136. data/lib/contrast/components/service.rb +55 -0
  137. data/lib/contrast/components/settings.rb +12 -4
  138. data/lib/contrast/config/base_configuration.rb +1 -1
  139. data/lib/contrast/config/protect_rules_configuration.rb +17 -3
  140. data/lib/contrast/config/server_configuration.rb +1 -1
  141. data/lib/contrast/config.rb +0 -6
  142. data/lib/contrast/configuration.rb +81 -17
  143. data/lib/contrast/extension/assess/exec_trigger.rb +3 -1
  144. data/lib/contrast/extension/assess/marshal.rb +3 -2
  145. data/lib/contrast/extension/assess/string.rb +0 -1
  146. data/lib/contrast/extension/extension.rb +1 -1
  147. data/lib/contrast/framework/base_support.rb +0 -5
  148. data/lib/contrast/framework/grape/support.rb +1 -23
  149. data/lib/contrast/framework/manager.rb +0 -10
  150. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -6
  151. data/lib/contrast/framework/rails/support.rb +5 -58
  152. data/lib/contrast/framework/sinatra/support.rb +2 -21
  153. data/lib/contrast/logger/cef_log.rb +21 -3
  154. data/lib/contrast/logger/log.rb +1 -11
  155. data/lib/contrast/tasks/config.rb +4 -2
  156. data/lib/contrast/utils/assess/event_limit_utils.rb +28 -12
  157. data/lib/contrast/utils/assess/trigger_method_utils.rb +10 -18
  158. data/lib/contrast/utils/findings.rb +6 -5
  159. data/lib/contrast/utils/hash_digest.rb +9 -24
  160. data/lib/contrast/utils/hash_digest_extend.rb +6 -6
  161. data/lib/contrast/utils/invalid_configuration_util.rb +21 -58
  162. data/lib/contrast/utils/log_utils.rb +47 -17
  163. data/lib/contrast/utils/net_http_base.rb +7 -8
  164. data/lib/contrast/utils/patching/policy/patch_utils.rb +3 -2
  165. data/lib/contrast/utils/stack_trace_utils.rb +0 -25
  166. data/lib/contrast/utils/string_utils.rb +9 -0
  167. data/lib/contrast/utils/telemetry_client.rb +13 -7
  168. data/lib/contrast.rb +5 -10
  169. metadata +39 -28
  170. data/lib/contrast/agent/reporting/reporting_events/trace_event_source.rb +0 -30
  171. data/lib/contrast/agent/reporting/reporting_utilities/dtm_message.rb +0 -43
  172. data/lib/contrast/api/decorators/activity.rb +0 -33
  173. data/lib/contrast/api/decorators/architecture_component.rb +0 -36
  174. data/lib/contrast/api/decorators/finding.rb +0 -29
  175. data/lib/contrast/api/decorators/route_coverage.rb +0 -91
  176. data/lib/contrast/api/decorators/trace_event.rb +0 -120
  177. data/lib/contrast/api/decorators/trace_event_object.rb +0 -63
  178. data/lib/contrast/api/decorators/trace_event_signature.rb +0 -69
  179. data/lib/contrast/api/decorators/trace_taint_range.rb +0 -52
  180. data/lib/contrast/config/assess_configuration.rb +0 -93
  181. data/lib/contrast/config/assess_rules_configuration.rb +0 -32
  182. data/lib/contrast/config/root_configuration.rb +0 -90
  183. data/lib/contrast/config/ruby_configuration.rb +0 -81
  184. data/lib/contrast/config/service_configuration.rb +0 -49
  185. data/lib/contrast/utils/preflight_util.rb +0 -13
@@ -180,7 +180,6 @@ module Contrast
180
180
  else
181
181
  request_handler.ruleset.postfilter
182
182
  request_handler.report_activity
183
- request_handler.send_activity_messages # TODO: RUBY-1438 -- remove
184
183
  end
185
184
  end
186
185
  # unsuccessful attack
@@ -33,6 +33,10 @@ module Contrast
33
33
  clazz = object.is_a?(Module) ? object : object.cs__class
34
34
  class_name = clazz.cs__name
35
35
  rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, class_name, method, command)
36
+ # invoke cmdi sub-rules.
37
+ rule.sub_rules.each do |sub_rule|
38
+ sub_rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, class_name, method, command)
39
+ end
36
40
  end
37
41
 
38
42
  protected
@@ -30,6 +30,7 @@ module Contrast
30
30
 
31
31
  sql = args[index]
32
32
  rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, database, sql)
33
+ rule.sub_rules.each { |sub_rule| sub_rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, sql) }
33
34
  end
34
35
 
35
36
  protected
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'contrast/components/logger'
5
5
  require 'contrast/components/scope'
6
+ require 'contrast/utils/object_share'
6
7
  require 'contrast/api/decorators/response_type'
7
8
 
8
9
  module Contrast
@@ -34,6 +35,11 @@ module Contrast
34
35
  Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED,
35
36
  Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED
36
37
  ]).cs__freeze
38
+ SUSPICIOUS_REPORTING_RULES = %w[
39
+ unsafe-file-upload
40
+ reflected-xss
41
+ sql-injection-semantic-dangerous-functions
42
+ ].cs__freeze
37
43
 
38
44
  attr_reader :mode
39
45
 
@@ -47,6 +53,12 @@ module Contrast
47
53
  cs__class.cs__name
48
54
  end
49
55
 
56
+ # Should return list of all sub_rules.
57
+ # Extend for each main rule any sub-rules.
58
+ def sub_rules
59
+ Contrast::Utils::ObjectShare::EMPTY_ARRAY
60
+ end
61
+
50
62
  def enabled?
51
63
  # 1. it is not enabled because protect is not enabled
52
64
  return false unless ::Contrast::AGENT.enabled?
@@ -169,9 +181,9 @@ module Contrast
169
181
  #
170
182
  # @param context [Contrast::Agent::RequestContext] the context of the
171
183
  # request in which this input is evaluated.
172
- # @param result [Contrast::Api::Dtm::AttackResult]
184
+ # @param result [Contrast::Agent::Reporting::AttackResult]
173
185
  def append_to_activity context, result
174
- context.activity.results << result if result
186
+ context.activity.attach_defend(result) if result
175
187
  end
176
188
 
177
189
  # With this we log to CEF
@@ -179,15 +191,11 @@ module Contrast
179
191
  # @param result [Contrast::Api::Dtm::AttackResult]
180
192
  # @param attack [Symbol] the type of message we want to send
181
193
  # @param value [String] the input value we want to log
182
- def cef_logging result, attack = :ineffective_attack, value = nil
183
- sample_to_json = Contrast::Api::Dtm::RaspRuleSample.to_controlled_hash(result.samples[0])
184
- outcome = if result.response.cs__is_a?(Hash)
185
- Contrast::Agent::Reporting::ResponseType.cs__const_get(result.response[:name])
186
- else
187
- Contrast::Api::Dtm::AttackResult::ResponseType.get_name_by_tag(result.response)
188
- end
189
- input_type = extract_input_type(sample_to_json[:user_input].input_type)
190
- input_value = value || sample_to_json[:user_input].value
194
+ def cef_logging result, attack = :ineffective_attack, value: nil
195
+ sample = result.samples[0]
196
+ outcome = result.response.to_s
197
+ input_type = sample.user_input.input_type.to_s
198
+ input_value = sample.user_input.value || value
191
199
  cef_logger.send(attack, result.rule_id, outcome, input_type, input_value)
192
200
  end
193
201
 
@@ -219,6 +227,12 @@ module Contrast
219
227
  for_rule.any? { |ex| ex.match_code?(stack) }
220
228
  end
221
229
 
230
+ # @param context [Contrast::Agent::RequestContext] the context of the
231
+ # request in which this input is evaluated.
232
+ def protect_excluded_by_url? context
233
+ Contrast::SETTINGS.excluder.protect_excluded_by_url?(context.request)
234
+ end
235
+
222
236
  # By default, rules do not have to find attackers as they do not have
223
237
  # Input Analysis. Any attack for the standard rule will be evaluated
224
238
  # at execution time. As such, those rules are expected to implement
@@ -238,9 +252,14 @@ module Contrast
238
252
  def update_successful_attack_response context, ia_result, result, attack_string = nil
239
253
  case mode
240
254
  when Contrast::Api::Settings::ProtectionRule::Mode::MONITOR
241
- result.response = Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED
255
+ # We are checking the result as the ia_result would not contain the sub-rules.
256
+ result.response = if SUSPICIOUS_REPORTING_RULES.include?(result&.rule_id)
257
+ Contrast::Agent::Reporting::ResponseType::SUSPICIOUS
258
+ else
259
+ Contrast::Agent::Reporting::ResponseType::MONITORED
260
+ end
242
261
  when Contrast::Api::Settings::ProtectionRule::Mode::BLOCK
243
- result.response = Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED
262
+ result.response = Contrast::Agent::Reporting::ResponseType::BLOCKED
244
263
  end
245
264
 
246
265
  ia_result.attack_count = ia_result.attack_count + 1 if ia_result
@@ -259,9 +278,9 @@ module Contrast
259
278
  def update_perimeter_attack_response context, ia_result, result
260
279
  if mode == Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER
261
280
  result.response = if blocked_rule?(ia_result)
262
- Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED
281
+ Contrast::Agent::Reporting::ResponseType::BLOCKED
263
282
  else
264
- Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED_AT_PERIMETER
283
+ Contrast::Agent::Reporting::ResponseType::BLOCKED_AT_PERIMETER
265
284
  end
266
285
  log_rule_matched(context, ia_result, result.response)
267
286
  elsif ia_result.nil? || ia_result.attack_count.zero?
@@ -276,13 +295,16 @@ module Contrast
276
295
  #
277
296
  # @param _context [Contrast::Agent::RequestContext] the context of
278
297
  # the current request
279
- # @return [Contrast::Api::Dtm::AttackResult]
298
+ # @return [Contrast::Agent::Reporting::AttackResult]
280
299
  def build_attack_result _context
281
- result = Contrast::Api::Dtm::AttackResult.new
300
+ result = Contrast::Agent::Reporting::AttackResult.new
282
301
  result.rule_id = rule_name
283
302
  result
284
303
  end
285
304
 
305
+ # @param sample [Contrast::Agent::Reporting::RaspRuleSample]
306
+ # @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists,
307
+ # in the case of multiple inputs being found to violate the protection criteria
286
308
  def append_stack sample, result
287
309
  return unless sample
288
310
  return unless STACK_COLLECTION_RESULTS.include?(result&.response)
@@ -293,6 +315,16 @@ module Contrast
293
315
  sample.stack_trace_elements += stack
294
316
  end
295
317
 
318
+ # @param context [Contrast::Agent::RequestContext] the context of the request in which this input is
319
+ # evaluated.
320
+ # @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the analysis of the input that was
321
+ # determined to be an attack
322
+ # @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists,
323
+ # in the case of multiple inputs being found to violate the protection criteria
324
+ # @param candidate_string [String] the value of the input which may be an attack
325
+ # @param kwargs [Hash] key - value pairs of context individual rules
326
+ # need to build out details to send to the Service to tell the
327
+ # story of the attack
296
328
  def append_sample context, ia_result, result, candidate_string, **kwargs
297
329
  return unless result
298
330
 
@@ -306,12 +338,23 @@ module Contrast
306
338
 
307
339
  # Override if rule can make use of the candidate string or kwargs to
308
340
  # build rasp rule sample.
341
+ #
342
+ # @param context [Contrast::Agent::RequestContext]
343
+ # @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the analysis of the input that was
344
+ # determined to be an attack
345
+ # @param _candidate_string [String] potential attack value/ input containing attack value
346
+ # @param _kwargs [Hash]
347
+ # @return [Contrast::Agent::Reporting::RaspRuleSample]
309
348
  def build_sample context, ia_result, _candidate_string, **_kwargs
310
349
  build_base_sample(context, ia_result)
311
350
  end
312
351
 
352
+ # @param context [Contrast::Agent::RequestContext]
353
+ # @param ia_result [Contrast::Api::Settings::InputAnalysisResult] the analysis of the input that was
354
+ # determined to be an attack
355
+ # @return [Contrast::Agent::Reporting::RaspRuleSample]
313
356
  def build_base_sample context, ia_result
314
- Contrast::Api::Dtm::RaspRuleSample.build(context, ia_result)
357
+ Contrast::Agent::Reporting::RaspRuleSample.build(context, ia_result)
315
358
  end
316
359
 
317
360
  def log_rule_matched _context, ia_result, response, _matched_string = nil
@@ -353,10 +396,7 @@ module Contrast
353
396
  # @param ia_result
354
397
  # @return [Boolean]
355
398
  def suspicious_rule? ia_result
356
- [
357
- Contrast::Agent::Protect::Rule::UnsafeFileUpload::NAME,
358
- Contrast::Agent::Protect::Rule::Xss::NAME
359
- ].include?(ia_result&.rule_id)
399
+ SUSPICIOUS_REPORTING_RULES.include?(ia_result&.rule_id)
360
400
  end
361
401
 
362
402
  # Handles the Response type for different Protect rules. Some rules need to report SUSPICIOUS over PROBED in
@@ -366,9 +406,9 @@ module Contrast
366
406
  # determined to be an attack
367
407
  def assign_reporter_response_type ia_result
368
408
  if suspicious_rule?(ia_result) && Contrast::CONTRAST_SERVICE.use_agent_communication?
369
- Contrast::Api::Dtm::AttackResult::ResponseType::SUSPICIOUS
409
+ Contrast::Agent::Reporting::ResponseType::SUSPICIOUS
370
410
  else
371
- Contrast::Api::Dtm::AttackResult::ResponseType::PROBED
411
+ Contrast::Agent::Reporting::ResponseType::PROBED
372
412
  end
373
413
  end
374
414
  end
@@ -29,6 +29,7 @@ module Contrast
29
29
  end
30
30
 
31
31
  return false if protect_excluded_by_code?
32
+ return false if protect_excluded_by_url?(context)
32
33
 
33
34
  true
34
35
  end
@@ -6,40 +6,36 @@ require 'contrast/utils/stack_trace_utils'
6
6
  require 'contrast/utils/object_share'
7
7
  require 'contrast/components/logger'
8
8
  require 'contrast/agent/reporting/input_analysis/input_type'
9
+ require 'contrast/agent/protect/rule/cmdi/cmdi_base_rule'
10
+ require 'contrast/agent/protect/rule/cmdi/cmdi_backdoors'
9
11
 
10
12
  module Contrast
11
13
  module Agent
12
14
  module Protect
13
15
  module Rule
14
16
  # The Ruby implementation of the Protect Command Injection rule.
15
- class CmdInjection < Contrast::Agent::Protect::Rule::BaseService
17
+ class CmdInjection < Contrast::Agent::Protect::Rule::CmdiBaseRule
16
18
  include Contrast::Components::Logger::InstanceMethods
17
19
  include Contrast::Agent::Reporting::InputType
18
-
19
20
  NAME = 'cmd-injection'
20
- CHAINED_COMMAND_CHARS = /[;&|<>]/.cs__freeze
21
- APPLICABLE_USER_INPUTS = [
22
- BODY, COOKIE_VALUE, HEADER, PARAMETER_NAME,
23
- PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE,
24
- MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
25
- ].cs__freeze
26
-
27
- class << self
28
- # @param attack_sample [Contrast::Api::Dtm::RaspRuleSample]
29
- # @return [Hash] the details for this specific rule
30
- def extract_details attack_sample
31
- {
32
- command: attack_sample.cmdi.command,
33
- startIndex: attack_sample.cmdi.start_idx,
34
- endIndex: attack_sample.cmdi.end_idx
35
- }
36
- end
37
- end
21
+ SUB_RULES = [Contrast::Agent::Protect::Rule::CmdiBackdoors.new].cs__freeze
38
22
 
39
23
  def rule_name
40
24
  NAME
41
25
  end
42
26
 
27
+ def sub_rules
28
+ SUB_RULES
29
+ end
30
+
31
+ # CMDI infilter:
32
+ #
33
+ # @param context [Contrast::Agent::RequestContext] current request context
34
+ # @param classname [String] Name of the class
35
+ # @param method [String] name of the method triggering the rule
36
+ # @param command [String] potential dangerous command executed.
37
+ # @raise [Contrast::SecurityException] if the rule mode is set
38
+ # to BLOCK and valid cdmi is detected.
43
39
  def infilter context, classname, method, command
44
40
  return unless infilter?(context)
45
41
 
@@ -62,90 +58,8 @@ module Contrast
62
58
 
63
59
  return unless blocked?
64
60
 
65
- raise(Contrast::SecurityException.new(self,
66
- 'Command Injection rule triggered. '\
67
- "Call to #{ classname }.#{ method } blocked."))
68
- end
69
-
70
- def build_attack_with_match context, input_analysis_result, result, candidate_string, **kwargs
71
- if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
72
- mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
73
-
74
- return result
75
- end
76
-
77
- result ||= build_attack_result(context)
78
- update_successful_attack_response(context, input_analysis_result, result, candidate_string)
79
- append_sample(context, input_analysis_result, result, candidate_string, **kwargs)
80
- result
81
- end
82
-
83
- protected
84
-
85
- # Because results are not necessarily on the context across
86
- # processes; extract early and pass into the method
87
- def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs
88
- logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string)
89
- result = super(context, potential_attack_string, ia_results, **kwargs)
90
- if result.nil? && potential_attack_string
91
- result = find_probable_attacker(context, potential_attack_string, ia_results, **kwargs)
92
- end
93
- result
94
- end
95
-
96
- # Build a subclass of the RaspRuleSample using the query string and the
97
- # evaluation
98
- def build_sample context, input_analysis_result, candidate_string, **_kwargs
99
- sample = build_base_sample(context, input_analysis_result)
100
- sample.cmdi = Contrast::Api::Dtm::CmdInjectionDetails.new
101
-
102
- command = candidate_string || input_analysis_result.value
103
- command = Contrast::Utils::StringUtils.protobuf_safe_string(command)
104
- sample.cmdi.command = command
105
- sample.cmdi.end_idx = command.length
106
-
107
- # This is a special case where the user input is UNKNOWN_USER_INPUT but
108
- # we want to send the attack value
109
- if input_analysis_result.nil?
110
- ui = Contrast::Api::Dtm::UserInput.new
111
- ui.input_type = :UNKNOWN
112
- ui.value = command
113
- sample.user_input = ui
114
- end
115
-
116
- sample
117
- end
118
-
119
- private
120
-
121
- def report_command_execution context, command, **kwargs
122
- return unless report_any_command_execution?
123
- return if protect_excluded_by_code?
124
-
125
- build_attack_with_match(context, nil, nil, command, **kwargs)
126
- end
127
-
128
- def find_probable_attacker context, potential_attack_string, ia_results, **kwargs
129
- return unless chained_command?(potential_attack_string)
130
-
131
- likely_attacker = ia_results.find { |input_analysis_result| chained_command?(input_analysis_result.value) }
132
- return unless likely_attacker
133
-
134
- build_attack_with_match(context, likely_attacker, nil, potential_attack_string, **kwargs)
135
- end
136
-
137
- def chained_command? command
138
- CHAINED_COMMAND_CHARS.match(command)
139
- end
140
-
141
- # Part of the Hardening for Command Injection detection is the
142
- # ability to detect and prevent any command execution from within the
143
- # application. This check determines if that hardening has been
144
- # enabled.
145
- # @return [Boolean] if the agent should report all command
146
- # executions.
147
- def report_any_command_execution?
148
- ::Contrast::PROTECT.report_any_command_execution?
61
+ # Raise cmdi error
62
+ raise_error(classname, method)
149
63
  end
150
64
  end
151
65
  end
@@ -0,0 +1,129 @@
1
+ # Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/protect/rule/base_service'
5
+ require 'contrast/agent/request_context'
6
+ require 'contrast/utils/object_share'
7
+ require 'contrast/agent/protect/rule/cmdi/cmdi_base_rule'
8
+ require 'contrast/agent/protect/rule/cmd_injection'
9
+
10
+ module Contrast
11
+ module Agent
12
+ module Protect
13
+ module Rule
14
+ # The Ruby implementation of the Protect Command Injection Command
15
+ # Backdoors sub-rule.
16
+ class CmdiBackdoors < Contrast::Agent::Protect::Rule::CmdiBaseRule
17
+ NAME = 'cmd-injection-command-backdoors'
18
+ MATCHES = %w[/bin/bash-c /bin/sh-c sh-c bash-c].cs__freeze
19
+
20
+ def rule_name
21
+ NAME
22
+ end
23
+
24
+ # CMDI Backdoors infilter:
25
+ # This rule does not have input classification.
26
+ # If a value matches the CMDI applicable input types and it's length is > 2
27
+ # we can check if it's used as command backdoors.
28
+ #
29
+ # @param context [Contrast::Agent::RequestContext] current request contest
30
+ # @param classname [String] Name of the class
31
+ # @param method [String] name of the method triggering the rule
32
+ # @param command [String] potential dangerous command executed.
33
+ # @raise [Contrast::SecurityException] if the rule mode ise set
34
+ # to BLOCK and valid cdmi is detected.
35
+ def infilter context, classname, method, command
36
+ return unless (ia_results = gather_ia_results(context))
37
+ return unless backdoors_match?(command)
38
+ return unless (likely_attacker = match_applicable_input_type(ia_results, command))
39
+ return if protect_excluded_by_code?
40
+ return unless (result = build_attack_with_match(context, likely_attacker, nil, command,
41
+ **{ classname: classname, method: method }))
42
+
43
+ append_to_activity(context, result)
44
+ cef_logging(result, :successful_attack)
45
+ return unless blocked?
46
+
47
+ raise_error(classname, method)
48
+ end
49
+
50
+ # @param context [Contrast::Agent::RequestContext]
51
+ def infilter? context
52
+ return false unless enabled?
53
+ # This rule does not have input tracing stage so we need to check the results of
54
+ # the main rule instead.
55
+ return false unless context&.speedracer_input_analysis&.results&.any? do |result|
56
+ result.rule_id == Contrast::Agent::Protect::Rule::CmdInjection::NAME
57
+ end
58
+
59
+ return false if protect_excluded_by_code?
60
+
61
+ true
62
+ end
63
+
64
+ protected
65
+
66
+ # Used to customize the raised error message.
67
+ #
68
+ # @param classname [String] Name of the class
69
+ # @param method [String] name of the method triggering the rule
70
+ # @raise [Contrast::SecurityException]
71
+ def raise_error classname, method
72
+ raise(Contrast::SecurityException.new(self,
73
+ 'Command Injection Command Backdoor rule triggered. '\
74
+ "Call to #{ classname }.#{ method } blocked."))
75
+ end
76
+
77
+ private
78
+
79
+ # Check to see if value is backdoor match
80
+ #
81
+ # @param potential_attack_string [String]
82
+ def backdoors_match? potential_attack_string
83
+ return false unless potential_attack_string && potential_attack_string.length > 2
84
+ return false unless matches_shell_parameter?(potential_attack_string)
85
+
86
+ true
87
+ end
88
+
89
+ # Checks to see if input is used as parameter for a shell cmd.
90
+ #
91
+ # @param cmd [String]
92
+ # @return [Boolean]
93
+ def matches_shell_parameter? cmd
94
+ normalize_cmd = cmd.delete(Contrast::Utils::ObjectShare::SPACE)
95
+ MATCHES.each do |match|
96
+ return true if normalize_cmd.include?(match)
97
+ end
98
+ false
99
+ end
100
+
101
+ # Check to see if the user input is one of the applicable types.
102
+ # With Agent_ia if we are here and CMDI is being analyzed then
103
+ # the applicable input type is already checked before input
104
+ # classification. Only thing left is the match check.
105
+ #
106
+ # @param ia_results [Contrast::Api::Settings::InputAnalysisResult] gathered results
107
+ # @param cmd [String] potential attack vector
108
+ # @return [Contrast::Api::Settings::InputAnalysisResult, nil] matched input_type
109
+ def match_applicable_input_type ia_results, cmd
110
+ ia_results.each do |ia_result|
111
+ return ia_result if ia_result.value == cmd
112
+ end
113
+ nil
114
+ end
115
+
116
+ # Backdoors does not have input classification so we check for match within the
117
+ # result for the main rule - CMDI.
118
+ #
119
+ # @param context [Contrast::Agent::RequestContext]
120
+ def gather_ia_results context
121
+ context.speedracer_input_analysis.results.select do |ia_result|
122
+ ia_result.rule_id == Contrast::Agent::Protect::Rule::CmdInjection::NAME
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,169 @@
1
+ # Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/reporting/details/cmd_injection_details'
5
+ require 'contrast/agent/reporting/attack_result/user_input'
6
+
7
+ module Contrast
8
+ module Agent
9
+ module Protect
10
+ module Rule
11
+ # The Ruby implementation of the Protect Command Injection Semantic
12
+ # Dangerous Path sub-rule. This rule should report
13
+ class CmdiBaseRule < Contrast::Agent::Protect::Rule::BaseService
14
+ include Contrast::Components::Logger::InstanceMethods
15
+ include Contrast::Agent::Reporting::InputType
16
+
17
+ CHAINED_COMMAND_CHARS = /[;&|<>]/.cs__freeze
18
+ APPLICABLE_USER_INPUTS = [
19
+ BODY, COOKIE_VALUE, HEADER, PARAMETER_NAME,
20
+ PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE,
21
+ MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
22
+ ].cs__freeze
23
+
24
+ class << self
25
+ # @param attack_sample [Contrast::Api::Dtm::RaspRuleSample]
26
+ # @return [Hash] the details for this specific rule
27
+ def extract_details attack_sample
28
+ {
29
+ command: attack_sample.cmdi.command,
30
+ startIndex: attack_sample.cmdi.start_idx,
31
+ endIndex: attack_sample.cmdi.end_idx
32
+ }
33
+ end
34
+ end
35
+
36
+ # CMDI infilter:
37
+ #
38
+ # @param context [Contrast::Agent::RequestContext] current request contest
39
+ # @param classname [String] Name of the class
40
+ # @param method [String] name of the method triggering the rule
41
+ # @param command [String] potential dangerous command executed.
42
+ # @raise [Contrast::SecurityException] if the rule mode ise set
43
+ # to BLOCK and valid cdmi is detected.
44
+ def infilter context, classname, method, command
45
+ if ::Contrast::APP_CONTEXT.in_new_process?
46
+ logger.trace('Running cmd-injection infilter within new process - creating new context')
47
+ context = Contrast::Agent::RequestContext.new(context.request.rack_request)
48
+ Contrast::Agent::REQUEST_TRACKER.update_current_context(context)
49
+ end
50
+ return unless infilter?(context)
51
+
52
+ result = find_attacker_with_results(context, command, nil, **{ classname: classname, method: method })
53
+ result ||= report_command_execution(context, command, **{ classname: classname, method: method })
54
+ return unless result
55
+
56
+ append_to_activity(context, result)
57
+ cef_logging(result, :successful_attack)
58
+ return unless blocked?
59
+
60
+ raise_error(classname, method)
61
+ end
62
+
63
+ def build_attack_with_match(context,
64
+ input_analysis_result,
65
+ result,
66
+ candidate_string,
67
+ **kwargs)
68
+ if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
69
+ mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
70
+
71
+ return result
72
+ end
73
+
74
+ result ||= build_attack_result(context)
75
+ update_successful_attack_response(context, input_analysis_result, result, candidate_string)
76
+ append_sample(context, input_analysis_result, result, candidate_string, **kwargs)
77
+ result
78
+ end
79
+
80
+ protected
81
+
82
+ # Used to customize the raised error message.
83
+ #
84
+ # @param classname [String] Name of the class
85
+ # @param method [String] name of the method triggering the rule
86
+ # @raise [Contrast::SecurityException]
87
+ def raise_error classname, method
88
+ raise(Contrast::SecurityException.new(self,
89
+ 'Command Injection Rule triggered. '\
90
+ "Call to #{ classname }.#{ method } blocked."))
91
+ end
92
+
93
+ # Allows for the InputAnalysis from service to be extracted early.
94
+ # Because results are not necessarily on the context across
95
+ # processes; extract early and pass into the method
96
+ #
97
+ # @param context [Contrast::Agent::RequestContext]
98
+ # @param potential_attack_string [String, nil]
99
+ # @param ia_results [Array<Contrast::Api::Settings::InputAnalysis>]
100
+ # @param **kwargs
101
+ # @return [Contrast::Api::Dtm::AttackResult, nil]
102
+ def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs
103
+ logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string)
104
+ result = super(context, potential_attack_string, ia_results, **kwargs) if ia_results
105
+ if result.nil? && potential_attack_string
106
+ result = find_probable_attacker(context, potential_attack_string, ia_results, **kwargs)
107
+ end
108
+ result
109
+ end
110
+
111
+ # Build a subclass of the RaspRuleSample using the query string and the
112
+ # evaluation
113
+ def build_sample context, input_analysis_result, candidate_string, **_kwargs
114
+ sample = build_base_sample(context, input_analysis_result)
115
+ sample.details = Contrast::Agent::Reporting::Details::CmdInjectionDetails.new
116
+
117
+ command = candidate_string || input_analysis_result.value
118
+ command = Contrast::Utils::StringUtils.protobuf_safe_string(command)
119
+ sample.details.cmd = command
120
+ sample.details.end_idx = command.length
121
+
122
+ # This is a special case where the user input is UNKNOWN_USER_INPUT but
123
+ # we want to send the attack value
124
+ if input_analysis_result.nil?
125
+ ui = Contrast::Agent::Reporting::UserInput.new
126
+ ui.input_type = :UNKNOWN
127
+ ui.value = command
128
+ sample.user_input = ui
129
+ end
130
+
131
+ sample
132
+ end
133
+
134
+ private
135
+
136
+ def report_command_execution context, command, **kwargs
137
+ return unless report_any_command_execution?
138
+ return if protect_excluded_by_code?
139
+
140
+ build_attack_with_match(context, nil, nil, command, **kwargs)
141
+ end
142
+
143
+ def find_probable_attacker context, potential_attack_string, ia_results, **kwargs
144
+ return unless chained_command?(potential_attack_string)
145
+
146
+ likely_attacker = ia_results.find { |input_analysis_result| chained_command?(input_analysis_result.value) }
147
+ return unless likely_attacker
148
+
149
+ build_attack_with_match(context, likely_attacker, nil, potential_attack_string, **kwargs)
150
+ end
151
+
152
+ def chained_command? command
153
+ CHAINED_COMMAND_CHARS.match(command)
154
+ end
155
+
156
+ # Part of the Hardening for Command Injection detection is the
157
+ # ability to detect and prevent any command execution from within the
158
+ # application. This check determines if that hardening has been
159
+ # enabled.
160
+ # @return [Boolean] if the agent should report all command
161
+ # executions.
162
+ def report_any_command_execution?
163
+ ::Contrast::PROTECT.report_any_command_execution?
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end