contrast-agent 6.8.0 → 6.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/lib/contrast/agent/assess/policy/trigger_method.rb +1 -1
  4. data/lib/contrast/agent/assess/property/evented.rb +11 -11
  5. data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -1
  6. data/lib/contrast/agent/assess.rb +0 -1
  7. data/lib/contrast/agent/excluder.rb +1 -1
  8. data/lib/contrast/agent/middleware.rb +12 -4
  9. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +76 -83
  10. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +121 -0
  11. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +2 -0
  12. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +6 -3
  13. data/lib/contrast/agent/protect/policy/applies_path_traversal_rule.rb +3 -0
  14. data/lib/contrast/agent/protect/policy/applies_sqli_rule.rb +3 -0
  15. data/lib/contrast/agent/protect/policy/rule_applicator.rb +12 -0
  16. data/lib/contrast/agent/protect/rule/base.rb +21 -7
  17. data/lib/contrast/agent/protect/rule/base_service.rb +6 -0
  18. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +1 -1
  19. data/lib/contrast/agent/protect/rule/bot_blocker.rb +9 -1
  20. data/lib/contrast/agent/protect/rule/cmd_injection.rb +8 -7
  21. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +8 -0
  22. data/lib/contrast/agent/protect/rule/cmdi/cmdi_chained_command.rb +0 -5
  23. data/lib/contrast/agent/protect/rule/cmdi/cmdi_dangerous_path.rb +0 -5
  24. data/lib/contrast/agent/protect/rule/deserialization.rb +2 -2
  25. data/lib/contrast/agent/protect/rule/no_sqli.rb +24 -2
  26. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +1 -1
  27. data/lib/contrast/agent/protect/rule/path_traversal.rb +12 -3
  28. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +0 -1
  29. data/lib/contrast/agent/protect/rule/sqli.rb +10 -13
  30. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +6 -2
  31. data/lib/contrast/agent/protect/rule/unsafe_file_upload.rb +20 -0
  32. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +1 -1
  33. data/lib/contrast/agent/protect/rule/xss.rb +9 -0
  34. data/lib/contrast/agent/protect/rule/xxe.rb +2 -2
  35. data/lib/contrast/agent/protect/rule.rb +0 -3
  36. data/lib/contrast/agent/reporting/attack_result/rasp_rule_sample.rb +1 -1
  37. data/lib/contrast/agent/reporting/attack_result/user_input.rb +0 -1
  38. data/lib/contrast/agent/reporting/details/details.rb +0 -1
  39. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +12 -0
  40. data/lib/contrast/agent/reporting/report.rb +2 -0
  41. data/lib/contrast/agent/reporting/reporter.rb +42 -7
  42. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +5 -6
  43. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +24 -7
  44. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_activity.rb +20 -5
  45. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample.rb +0 -1
  46. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample_activity.rb +5 -0
  47. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +10 -1
  48. data/lib/contrast/agent/reporting/reporting_events/application_inventory.rb +2 -1
  49. data/lib/contrast/agent/reporting/reporting_events/application_inventory_activity.rb +6 -1
  50. data/lib/contrast/agent/reporting/reporting_events/application_reporting_event.rb +10 -0
  51. data/lib/contrast/agent/reporting/reporting_events/application_settings.rb +40 -0
  52. data/lib/contrast/agent/reporting/reporting_events/finding.rb +2 -2
  53. data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +239 -93
  54. data/lib/contrast/agent/reporting/reporting_events/finding_event_signature.rb +10 -23
  55. data/lib/contrast/agent/reporting/reporting_events/finding_event_source.rb +10 -9
  56. data/lib/contrast/agent/reporting/reporting_events/observed_route.rb +12 -0
  57. data/lib/contrast/agent/reporting/reporting_events/server_reporting_event.rb +8 -0
  58. data/lib/contrast/agent/reporting/reporting_events/server_settings.rb +40 -0
  59. data/lib/contrast/agent/reporting/reporting_utilities/endpoints.rb +6 -0
  60. data/lib/contrast/agent/reporting/reporting_utilities/ng_response_extractor.rb +137 -0
  61. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +52 -2
  62. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +8 -4
  63. data/lib/contrast/agent/reporting/reporting_utilities/response_extractor.rb +105 -58
  64. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +9 -7
  65. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +143 -49
  66. data/lib/contrast/agent/reporting/reporting_workers/application_server_worker.rb +46 -0
  67. data/lib/contrast/agent/reporting/reporting_workers/reporter_heartbeat.rb +51 -0
  68. data/lib/contrast/agent/reporting/reporting_workers/reporting_workers.rb +14 -0
  69. data/lib/contrast/agent/reporting/reporting_workers/server_settings_worker.rb +46 -0
  70. data/lib/contrast/agent/reporting/settings/assess.rb +14 -1
  71. data/lib/contrast/agent/reporting/settings/assess_rule.rb +18 -0
  72. data/lib/contrast/agent/reporting/settings/assess_server_feature.rb +14 -2
  73. data/lib/contrast/agent/reporting/settings/helpers.rb +9 -0
  74. data/lib/contrast/agent/reporting/settings/protect.rb +17 -12
  75. data/lib/contrast/agent/reporting/settings/protect_rule.rb +18 -0
  76. data/lib/contrast/agent/reporting/settings/protect_server_feature.rb +40 -3
  77. data/lib/contrast/agent/reporting/settings/rule_definition.rb +3 -0
  78. data/lib/contrast/agent/reporting/settings/security_logger.rb +77 -0
  79. data/lib/contrast/agent/reporting/settings/sensitive_data_masking.rb +1 -1
  80. data/lib/contrast/agent/reporting/settings/server_features.rb +9 -0
  81. data/lib/contrast/agent/reporting/settings/syslog.rb +34 -5
  82. data/lib/contrast/agent/reporting/settings/virtual_patch.rb +56 -0
  83. data/lib/contrast/agent/reporting/settings/virtual_patch_condition.rb +47 -0
  84. data/lib/contrast/agent/request.rb +1 -0
  85. data/lib/contrast/agent/request_context_extend.rb +20 -0
  86. data/lib/contrast/agent/request_handler.rb +5 -10
  87. data/lib/contrast/agent/telemetry/base.rb +11 -10
  88. data/lib/contrast/agent/telemetry/events/exceptions/obfuscate.rb +108 -103
  89. data/lib/contrast/agent/telemetry/events/exceptions/telemetry_exception_event.rb +1 -1
  90. data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +1 -1
  91. data/lib/contrast/agent/thread_watcher.rb +46 -6
  92. data/lib/contrast/agent/version.rb +1 -1
  93. data/lib/contrast/agent.rb +18 -0
  94. data/lib/contrast/agent_lib/api/init.rb +1 -7
  95. data/lib/contrast/agent_lib/api/input_tracing.rb +2 -4
  96. data/lib/contrast/agent_lib/interface.rb +1 -16
  97. data/lib/contrast/agent_lib/interface_base.rb +52 -39
  98. data/lib/contrast/agent_lib/return_types/eval_result.rb +2 -2
  99. data/lib/contrast/api/communication/connection_status.rb +15 -0
  100. data/lib/contrast/components/agent.rb +34 -0
  101. data/lib/contrast/components/api.rb +23 -0
  102. data/lib/contrast/components/app_context.rb +23 -3
  103. data/lib/contrast/components/assess.rb +60 -8
  104. data/lib/contrast/components/assess_rules.rb +18 -0
  105. data/lib/contrast/components/base.rb +40 -0
  106. data/lib/contrast/components/config/sources.rb +95 -0
  107. data/lib/contrast/components/config.rb +18 -1
  108. data/lib/contrast/components/heap_dump.rb +10 -0
  109. data/lib/contrast/components/inventory.rb +15 -2
  110. data/lib/contrast/components/logger.rb +18 -0
  111. data/lib/contrast/components/polling.rb +39 -0
  112. data/lib/contrast/components/protect.rb +48 -1
  113. data/lib/contrast/components/ruby_component.rb +15 -0
  114. data/lib/contrast/components/sampling.rb +70 -13
  115. data/lib/contrast/components/security_logger.rb +13 -0
  116. data/lib/contrast/components/settings.rb +120 -10
  117. data/lib/contrast/config/certification_configuration.rb +14 -0
  118. data/lib/contrast/config/config.rb +46 -0
  119. data/lib/contrast/config/diagnostics.rb +114 -0
  120. data/lib/contrast/config/diagnostics_tools.rb +98 -0
  121. data/lib/contrast/config/effective_config.rb +65 -0
  122. data/lib/contrast/config/effective_config_value.rb +32 -0
  123. data/lib/contrast/config/exception_configuration.rb +12 -0
  124. data/lib/contrast/config/protect_rule_configuration.rb +2 -2
  125. data/lib/contrast/config/protect_rules_configuration.rb +7 -6
  126. data/lib/contrast/config/request_audit_configuration.rb +13 -0
  127. data/lib/contrast/config/server_configuration.rb +41 -2
  128. data/lib/contrast/configuration.rb +28 -2
  129. data/lib/contrast/extension/assess/array.rb +3 -3
  130. data/lib/contrast/extension/assess/erb.rb +1 -1
  131. data/lib/contrast/extension/assess/regexp.rb +2 -2
  132. data/lib/contrast/logger/aliased_logging.rb +48 -15
  133. data/lib/contrast/utils/assess/event_limit_utils.rb +31 -9
  134. data/lib/contrast/utils/assess/trigger_method_utils.rb +5 -4
  135. data/lib/contrast/utils/hash_digest.rb +2 -2
  136. data/lib/contrast/utils/input_classification_base.rb +21 -5
  137. data/lib/contrast/utils/reporting/application_activity_batch_utils.rb +81 -0
  138. data/lib/contrast/utils/routes_sent.rb +60 -0
  139. data/lib/contrast/utils/telemetry.rb +1 -1
  140. data/lib/contrast/utils/telemetry_client.rb +2 -3
  141. data/lib/contrast/utils/timer.rb +16 -0
  142. data/lib/contrast.rb +3 -1
  143. data/resources/protect/policy.json +8 -0
  144. data/ruby-agent.gemspec +6 -2
  145. metadata +43 -24
  146. data/lib/contrast/agent/assess/contrast_event.rb +0 -157
  147. data/lib/contrast/agent/assess/events/event_factory.rb +0 -34
  148. data/lib/contrast/agent/assess/events/source_event.rb +0 -46
  149. data/lib/contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification.rb +0 -96
  150. data/lib/contrast/agent/protect/rule/http_method_tampering.rb +0 -83
  151. data/lib/contrast/agent/reporting/details/http_method_tempering_details.rb +0 -27
  152. data/lib/contrast/agent/reporting/reporter_heartbeat.rb +0 -53
  153. data/lib/contrast/agent/reporting/reporting_events/server_activity.rb +0 -36
  154. data/lib/contrast/agent_lib/api/method_tempering.rb +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c32847e196cdcf1540bd39373f9327a46e50cb1c4be6516f2ad5664696c0221
4
- data.tar.gz: 03b9bc37cf6b8fe3fd0662f120f6f24cc9faf868cf91c7fb698ccbcb52f15aac
3
+ metadata.gz: 624b29e40ff797608bb6fef0ab00377cc1bc3e6756af01063d4768fad53bfbfa
4
+ data.tar.gz: 7380498d855e3f6b8a7387ee98351d118b6b70b7bc332536bf1124a870c1a48c
5
5
  SHA512:
6
- metadata.gz: 204a13ebf2cc20d9302d55a1d2201cc0f8f2ba35b5bf1b3392f75c0636f8ab5224de54106af8349c16da111d86f2381e894d8925eccf2df1e46e4d04b99eb7be
7
- data.tar.gz: ac50424a98754b82bab6576b7205cdb3f6243f5dc6aa9c55f817f15cfba61e1f0858c27a66efd18457442f0b4f2e8ea42cdfcbecdd67c6941edcd86c77b11164
6
+ metadata.gz: 44d0b0cc41d92f58cf048212295fdef8367a4aa4475e3d035c9ae09c80bbcad9de08ba2a63f321381de4af41fd90b581dca2710ef3641d8eb486e8664a3d18cf
7
+ data.tar.gz: cb2f9e2799f8ef7fff7da2ba6714e94022ad5495ddc57c448244c34f649cb70eb756d4f806c1f1274676cbdd37749bf0177cd8539b175ba7b8086857bd1f86a3
data/.gitignore CHANGED
@@ -23,7 +23,7 @@ ruby-spec
23
23
  mspec
24
24
 
25
25
  # rspec generated files
26
- /spec/dummy_files/*
26
+ /spec/dummy_files/
27
27
 
28
28
  # Funchook artifacts
29
29
  /ext/**/funchook.h
@@ -1,7 +1,6 @@
1
1
  # Copyright (c) 2022 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/assess/events/event_factory'
5
4
  require 'contrast/agent/assess/policy/trigger_validation/trigger_validation'
6
5
  require 'contrast/agent/excluder'
7
6
  require 'contrast/components/logger'
@@ -15,6 +14,7 @@ require 'contrast/agent/reporting/reporting_events/preflight_message'
15
14
  require 'contrast/agent/reporting/reporting_events/route_discovery'
16
15
  require 'contrast/agent/reporting/reporting_utilities/reporting_storage'
17
16
  require 'contrast/agent/reporting/reporting_utilities/build_preflight'
17
+ require 'contrast/utils/assess/event_limit_utils'
18
18
 
19
19
  module Contrast
20
20
  module Agent
@@ -1,8 +1,7 @@
1
1
  # Copyright (c) 2022 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/assess/events/event_factory'
5
- require 'contrast/agent/assess/events/source_event'
4
+ require 'contrast/agent/reporting/reporting_events/finding_event'
6
5
 
7
6
  module Contrast
8
7
  module Agent
@@ -25,7 +24,7 @@ module Contrast
25
24
  # the key used to accessed if from a map or nil if a type like
26
25
  # BODY
27
26
  def build_event event_data, source_type = nil, source_name = nil
28
- @event = Contrast::Agent::Assess::Events::EventFactory.build(event_data, source_type, source_name)
27
+ @event = Contrast::Agent::Reporting::FindingEvent.new(event_data, source_type, source_name)
29
28
  report_sources(event_data.tagged, @event)
30
29
  end
31
30
 
@@ -35,21 +34,22 @@ module Contrast
35
34
  # context's observed route
36
35
  #
37
36
  # @param tagged [Object] The Target of the Event
38
- # @param event [Contrast::Agent::Assess::Events::ContrastEvent]
37
+ # @param event [Contrast::Agent::Reporting::FindingEvent]
39
38
  def report_sources tagged, event
40
39
  return unless tagged && !tagged.to_s.empty?
41
- return unless event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent)
42
40
  return unless event.source_type
43
41
  return unless (current_request = Contrast::Agent::REQUEST_TRACKER.current)
44
42
 
45
- if current_request.observed_route.sources.any? do |source|
46
- source.type == event.source_type && source.name == event.source_name # rubocop:disable Security/Module/Name
47
- end
43
+ event.event_sources&.each do |event_source|
44
+ if current_request.observed_route.sources.any? do |source|
45
+ source.source_type == event_source.source_type && source.source_name == event_source.source_name
46
+ end
48
47
 
49
- return
50
- end
48
+ next
49
+ end
51
50
 
52
- current_request.observed_route.sources << event.event_source if event.event_source
51
+ current_request.observed_route.sources << event_source
52
+ end
53
53
  end
54
54
  end
55
55
  end
@@ -50,7 +50,7 @@ module Contrast
50
50
 
51
51
  potential_elements(section, element_start_str).flatten.each do |potential_element|
52
52
  next unless potential_element
53
- next unless element_openings.any? { |opening| potential_element.starts_with?(opening) }
53
+ next unless element_openings.any? { |opening| potential_element.start_with?(opening) }
54
54
 
55
55
  section_start = section.index(element_start_str, section_start)
56
56
  next unless section_start
@@ -18,7 +18,6 @@ module Contrast
18
18
  # reporting / tracking
19
19
  require 'contrast/agent/assess/properties'
20
20
  require 'contrast/agent/assess/tag'
21
- require 'contrast/agent/assess/events/event_factory'
22
21
  end
23
22
  end
24
23
  end
@@ -82,7 +82,7 @@ module Contrast
82
82
  event_sources = finding.events.flat_map(&:event_sources)
83
83
  event_sources.each do |event_source|
84
84
  return false unless rule_input_exclusions.any? do |exclusion|
85
- input_match?(exclusion, event_source.type, event_source.name) # rubocop:disable Security/Module/Name
85
+ input_match?(exclusion, event_source.source_type, event_source.source_name)
86
86
  end
87
87
  end
88
88
 
@@ -14,8 +14,9 @@ require 'contrast/utils/telemetry'
14
14
  require 'contrast/agent/request_handler'
15
15
  require 'contrast/agent/static_analysis'
16
16
  require 'contrast/agent/telemetry/events/startup_metrics_event'
17
+ require 'contrast/agent/protect/input_analyzer/input_analyzer'
17
18
  require 'contrast/utils/middleware_utils'
18
-
19
+ require 'contrast/utils/reporting/application_activity_batch_utils'
19
20
  require 'contrast/utils/timer'
20
21
 
21
22
  module Contrast
@@ -23,10 +24,11 @@ module Contrast
23
24
  # This class allows the Agent to plug into the Rack middleware stack. When the application is first started, we
24
25
  # initialize ourselves as a rack middleware inside of #initialize. Afterwards, we process each http request and
25
26
  # response as it goes through the middleware stack inside of #call.
26
- class Middleware
27
+ class Middleware # rubocop:disable Metrics/ClassLength
27
28
  include Contrast::Components::Logger::InstanceMethods
28
29
  include Contrast::Components::Scope::InstanceMethods
29
30
  include Contrast::Utils::MiddlewareUtils
31
+ include Contrast::Utils::Reporting::ApplicationActivityBatchUtils
30
32
 
31
33
  attr_reader :app
32
34
 
@@ -62,6 +64,7 @@ module Contrast
62
64
  # the Rack framework.
63
65
  def call env
64
66
  logger.trace_with_time('Elapsed time for Contrast::Agent::Middleware#call') do
67
+ ::Contrast::Agent::ThreadWatcher.check_before_start
65
68
  return app.call(env) unless ::Contrast::AGENT.enabled?
66
69
 
67
70
  Contrast::Agent.heapdump_util.start_thread!
@@ -169,17 +172,22 @@ module Contrast
169
172
  with_contrast_scope do
170
173
  context.extract_after(response) # update context with final response information
171
174
 
172
- # Build and report all collected findings prior response
173
175
  Contrast::Agent::FINDINGS.report_collected_findings unless Contrast::Agent::FINDINGS.collection.empty?
174
176
  # All protect rules, which are trigger but require response to be reported
175
177
  Contrast::Agent::EXPLOITS.report_recorded_exploits(context) unless Contrast::Agent::EXPLOITS.collection.empty?
178
+ # Process Worth Watching Inputs for v2 rules
179
+ Contrast::Agent.worth_watching_analyzer&.add_to_queue(context.agent_input_analysis)
180
+ # Now we can build the ia_results only for postfilter rules.
181
+ context.protect_postfilter_ia
176
182
 
177
183
  if Contrast::Agent.framework_manager.streaming?(env)
178
184
  context.reset_activity
179
185
  request_handler.stream_safe_postfilter
180
186
  else
181
187
  request_handler.ruleset.postfilter
182
- request_handler.report_activity
188
+ request_handler.report_observed_route
189
+ add_activity_to_batch(context.activity)
190
+ report_batch
183
191
  end
184
192
  end
185
193
  # unsuccessful attack
@@ -16,7 +16,6 @@ require 'contrast/agent/protect/rule/path_traversal'
16
16
  require 'contrast/agent/protect/rule/path_traversal/path_traversal_input_classification'
17
17
  require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
18
18
  require 'contrast/agent/protect/rule/xss'
19
- require 'contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification'
20
19
  require 'contrast/components/logger'
21
20
  require 'contrast/utils/object_share'
22
21
  require 'json'
@@ -29,7 +28,13 @@ module Contrast
29
28
  module InputAnalyzer
30
29
  DISPOSITION_NAME = 'name'
31
30
  DISPOSITION_FILENAME = 'filename'
32
- DISPOSITION_KEYS = %w[Content-Disposition CONTENT_DISPOSITION].cs__freeze
31
+ PREFILTER_RULES = %w[bot-blocker unsafe-file-upload reflected-xss].cs__freeze
32
+ INFILTER_RULES = %w[
33
+ sql-injection cmd-injection reflected-xss bot-blocker unsafe-file-upload path-traversal
34
+ nosql-injection
35
+ ].cs__freeze
36
+ POSTFILTER_RULES = %w[sql-injection cmd-injection reflected-xss path-traversal nosql-injection].cs__freeze
37
+ AGENTLIB_TIMEOUT = 5.cs__freeze
33
38
 
34
39
  class << self
35
40
  include Contrast::Agent::Reporting::InputType
@@ -37,44 +42,8 @@ module Contrast
37
42
  include Contrast::Utils::ObjectShare
38
43
  include Contrast::Components::Logger::InstanceMethods
39
44
 
40
- PROTECT_RULES = {
41
- sqli: {
42
- rule_name: 'sql-injection',
43
- classification: Contrast::Agent::Protect::Rule::SqliInputClassification
44
- },
45
- cmdi: {
46
- rule_name: 'cmd-injection',
47
- classification: Contrast::Agent::Protect::Rule::CmdiInputClassification
48
- },
49
- # method_tampering: {
50
- # rule_name: 'method-tampering',
51
- # classification: Contrast::Agent::Protect::Rule::HttpMethodTamperingInputClassification
52
- # },
53
- reflected_xss: {
54
- rule_name: Contrast::Agent::Protect::Rule::Xss::NAME,
55
- classification: Contrast::Agent::Protect::Rule::ReflectedXssInputClassification
56
- },
57
- bot_blocker: {
58
- rule_name: Contrast::Agent::Protect::Rule::BotBlocker::NAME,
59
- classification: Contrast::Agent::Protect::Rule::BotBlockerInputClassification
60
- },
61
- unsafe_file_upload: {
62
- rule_name: Contrast::Agent::Protect::Rule::UnsafeFileUpload::NAME,
63
- classification: Contrast::Agent::Protect::Rule::UnsafeFileUploadInputClassification
64
- },
65
- path_traversal: {
66
- rule_name: Contrast::Agent::Protect::Rule::PathTraversal::NAME,
67
- classification: Contrast::Agent::Protect::Rule::PathTraversalInputClassification
68
- },
69
- nosqli: {
70
- rule_name: Contrast::Agent::Protect::Rule::NoSqli::NAME,
71
- classification: Contrast::Agent::Protect::Rule::NoSqliInputClassification
72
- }
73
- }.cs__freeze
74
-
75
45
  # This method with analyze the user input from the context of the
76
- # current request and run each of the protect rules against all
77
- # found input types
46
+ # current request and return new ia with extracted input types.
78
47
  #
79
48
  # @param request [Contrast::Agent::Request] current request context.
80
49
  # @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
@@ -87,39 +56,8 @@ module Contrast
87
56
 
88
57
  input_analysis = Contrast::Agent::Reporting::InputAnalysis.new
89
58
  input_analysis.request = request
90
- # each rule against each input
91
- input_classification(inputs, input_analysis)
92
- input_analysis
93
- end
94
-
95
- private
96
-
97
- # classify input by rule implementation of worth_watching_v2 for the rules supporting it.
98
- #
99
- # @param inputs [String, Array<String>] user input to be analysed.
100
- # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Here we will keep all the results
101
- # for each protect rule.
102
- # @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
103
- def input_classification inputs, input_analysis
104
- # key = input type, value = user_input
105
- inputs.each do |input_type, value|
106
- next if value.nil? || value.empty?
107
-
108
- PROTECT_RULES.each do |_key, rule|
109
- protect_rule = Contrast::PROTECT.rule(rule[:rule_name])
110
- logger.debug("Rule #{ rule[:rule_name] } not recognised in Protect rules") if protect_rule.nil?
111
-
112
- # check if rule is enabled
113
- next unless protect_rule&.enabled?
114
-
115
- # method tampering doesn't take value
116
- if rule[:rule_name] == Contrast::Agent::Protect::Rule::HttpMethodTampering::NAME
117
- rule[:classification].send(:classify, rule[:rule_name], input_type, input_analysis)
118
- else
119
- rule[:classification].send(:classify, rule[:rule_name], input_type, value, input_analysis)
120
- end
121
- end
122
- end
59
+ # Save those for trigger time
60
+ input_analysis.inputs = extract_input(request)
123
61
  input_analysis
124
62
  end
125
63
 
@@ -146,22 +84,77 @@ module Contrast
146
84
  inputs
147
85
  end
148
86
 
87
+ # classify input by rule
88
+ #
89
+ # @param rule_id [String] name of the rule
90
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from
91
+ # analyze method.
92
+ def input_classification_for rule_id, input_analysis
93
+ return unless input_analysis&.inputs
94
+ return unless (protect_rule = Contrast::PROTECT.rule(rule_id)) && protect_rule.enabled?
95
+
96
+ input_analysis.inputs.each do |input_type, value|
97
+ next if value.nil? || value.empty?
98
+
99
+ # append to results.
100
+ protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
101
+ end
102
+ input_analysis
103
+ end
104
+
105
+ # classify input by array of rules. There is a timeout for the AgentLib analysis if not set it
106
+ # will use the default 5s.
107
+ #
108
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Here we will keep all the results
109
+ # for each protect rule.
110
+ # @param prefilter [Boolean] flag to set input analysis for prefilter rules only
111
+ # @param postfilter [Boolean] flag to set input analysis for postfilter rules.
112
+ # @param infilter [Boolean]
113
+ # @param interval [Integer] The timeout determined for the AgentLib analysis to be performed
114
+ # @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
115
+ # @raise [Timeout::Error] If timeout is met.
116
+ def input_classification(input_analysis,
117
+ prefilter: false,
118
+ postfilter: false,
119
+ infilter: false,
120
+ interval: AGENTLIB_TIMEOUT)
121
+ return unless input_analysis
122
+
123
+ rules = if prefilter
124
+ PREFILTER_RULES
125
+ elsif postfilter
126
+ POSTFILTER_RULES
127
+ else
128
+ INFILTER_RULES
129
+ end
130
+
131
+ rules.each do |rule_id|
132
+ # Check to see if rules is already triggered only for infilter:
133
+ next if input_analysis.triggered_rules.include?(rule_id) && infilter
134
+
135
+ Timeout.timeout(interval) do
136
+ input_classification_for(rule_id, input_analysis)
137
+ end
138
+ end
139
+ input_analysis
140
+ rescue Timeout::Error => e
141
+ logger.warn('AgentLib timed out when processing InputAnalysisResult', e, ia_result)
142
+ nil
143
+ end
144
+
145
+ private
146
+
149
147
  # Extract the filename and name of the Content Disposition Header.
150
148
  #
151
149
  # @param inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
152
150
  # @param request [Contrast::Agent::Request] current request context.
153
151
  def extract_multipart inputs, request
154
- disposition = request.rack_request.env[DISPOSITION_KEYS[0]]
155
- disposition = request.rack_request.env[DISPOSITION_KEYS[1]] if disposition.nil? || disposition.empty?
156
- return unless disposition
157
-
158
- pairs = {}
159
- disposition.split(SEMICOLON).each do |elem|
160
- new_pair = elem.strip.split(EQUALS, 2)
161
- pairs[new_pair[0].downcase] = new_pair[1] if new_pair
162
- end
163
- inputs[MULTIPART_NAME] = pairs[DISPOSITION_NAME]
164
- inputs[MULTIPART_FIELD_NAME] = pairs[DISPOSITION_FILENAME]
152
+ return unless (parsed_data = Rack::Multipart.parse_multipart(request.rack_request.env))
153
+
154
+ filename = parsed_data[DISPOSITION_FILENAME]
155
+ inputs[MULTIPART_FIELD_NAME] = filename[DISPOSITION_FILENAME.to_sym] if filename
156
+ name = filename[DISPOSITION_NAME.to_sym]
157
+ inputs[MULTIPART_NAME] = name if name
165
158
  end
166
159
  end
167
160
  end
@@ -0,0 +1,121 @@
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/worker_thread'
5
+ require 'contrast/agent/reporting/input_analysis/input_analysis_result'
6
+ require 'contrast/agent/reporting/input_analysis/score_level'
7
+ require 'contrast/agent/reporting/reporting_events/application_activity'
8
+ require 'contrast/utils/input_classification_base'
9
+
10
+ module Contrast
11
+ module Agent
12
+ module Protect
13
+ # WorthWatchingInputAnalyzer Perform analysis of input tracing v2 worthwatching results in a
14
+ # separate thread, should only be run at the end of the request.
15
+ # Currently only includes: cmd_injection & sqli_injection rules
16
+ class WorthWatchingInputAnalyzer < WorkerThread
17
+ include Timeout
18
+ include Contrast::Agent::Protect::Rule::InputClassificationBase
19
+
20
+ QUEUE_SIZE = 1000.cs__freeze
21
+ AGENTLIB_TIMEOUT = 5.cs__freeze
22
+ # max size of inputs to evaluate
23
+ INPUT_BYTESIZE_THRESHOLD = 100_000.cs__freeze
24
+ REPORT_INTERVAL_SECOND = 30.cs__freeze
25
+
26
+ # Thread that will process all the InputAnalysisResults that have a score level of WORTHWATCHING and
27
+ # sends results to TeamServer
28
+ def start_thread!
29
+ return if running?
30
+
31
+ @_thread = Contrast::Agent::Thread.new do
32
+ logger.info('[WorthWatchingAnalyzer] Starting thread.')
33
+ loop do
34
+ sleep(REPORT_INTERVAL_SECOND)
35
+ next if queue.empty?
36
+
37
+ report = false
38
+ # build attack_results for all infilter active protect rules.
39
+ results = build_results(queue.pop)
40
+ activity = Contrast::Agent::Reporting::ApplicationActivity.new
41
+ results.each do |result|
42
+ next unless (attack_result = eval_input(result))
43
+
44
+ activity.attach_defend(attack_result)
45
+ report = true
46
+ end
47
+ Contrast::Agent.reporter.send_event_immediately(activity) if report
48
+ rescue StandardError => e
49
+ logger.debug('[WorthWatchingAnalyzer] thread could not process result because of:', e)
50
+ end
51
+ end
52
+ end
53
+
54
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
55
+ def add_to_queue input_analysis
56
+ return unless input_analysis
57
+
58
+ if queue.size >= QUEUE_SIZE
59
+ logger.debug('[WorthWatchingAnalyzer] queue at max size, skip input_result')
60
+ return
61
+ end
62
+ # There will be no results here because of the delay of the protect rule analysis,
63
+ # we need to save the ia which contains the request and saved extracted user inputs to
64
+ # be evaluated on the thread rather than building results here. This way we allow the
65
+ # request to continue and will build the attack results later.
66
+ queue << input_analysis
67
+ end
68
+
69
+ private
70
+
71
+ # This method will build the attack results from the saved ia.
72
+ #
73
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
74
+ # @return attack_results [array<Contrast::Agent::Reporting::InputAnalysisResult>] all the results
75
+ # from the input analysis.
76
+ def build_results input_analysis
77
+ # Construct the input analysis for the all the infilter rules that were not triggered.
78
+ # There is a set timeout for each rule to be analyzed in. The infilter flag will make
79
+ # sure that if a rule is already triggered during the infilter phase it will not be analyzed
80
+ # now, making sure we don't report same rule twice.
81
+ Contrast::Agent::Protect::InputAnalyzer.input_classification(input_analysis, infilter: true)
82
+ results = []
83
+ input_analysis.results.reject { |val| val.score_level == SCORE_LEVEL::IGNORE }.
84
+ each do |ia_result|
85
+ results << ia_result
86
+ end
87
+
88
+ results
89
+ end
90
+
91
+ # @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the WorthWatching InputAnalysisResult
92
+ # @return [Contrast::Agent::Reporting::AttackResult, nil] InputAnalysisResult updated Result or nil
93
+ def eval_input ia_result
94
+ return build_attack_result(ia_result) unless ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD
95
+
96
+ logger.debug("[WorthWatchingAnalyzer] Skipping analysis: Input size is larger than
97
+ #{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
98
+ nil
99
+ end
100
+
101
+ # @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the updated InputAnalysisResult
102
+ # with a score of :DEFINITEATTACK
103
+ # @return [Contrast::Agent::Reporting::AttackResult] the attack result from
104
+ # this input
105
+ def build_attack_result ia_result
106
+ Contrast::PROTECT.rule(ia_result.rule_id).build_attack_without_match(nil, ia_result, nil)
107
+ end
108
+
109
+ def queue
110
+ @_queue ||= Queue.new
111
+ end
112
+
113
+ def delete_queue!
114
+ @_queue&.clear
115
+ @_queue&.close
116
+ @_queue = nil
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -32,6 +32,8 @@ module Contrast
32
32
 
33
33
  clazz = object.is_a?(Module) ? object : object.cs__class
34
34
  class_name = clazz.cs__name
35
+ # Get the ia for current rule:
36
+ apply_classification(rule_name, Contrast::Agent::REQUEST_TRACKER.current)
35
37
  rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, class_name, method, command)
36
38
  # invoke cmdi sub-rules.
37
39
  rule.sub_rules.each do |sub_rule|
@@ -20,6 +20,8 @@ module Contrast
20
20
  return unless valid_input?(args)
21
21
  return if skip_analysis?
22
22
 
23
+ # Get the ia for current rule:
24
+ apply_classification(rule_name, Contrast::Agent::REQUEST_TRACKER.current)
23
25
  database = properties['database']
24
26
  operations = args[0]
25
27
  context = Contrast::Agent::REQUEST_TRACKER.current
@@ -48,10 +50,11 @@ module Contrast
48
50
  end
49
51
 
50
52
  def handle_operation context, database, _action, operation
51
- data = extract_mongo_data(operation)
52
- return unless data && !data.empty?
53
+ # TODO: RUBY-1991 Expand NoSQLI triggers
54
+ # data = extract_mongo_data(operation)
55
+ # return unless data && !data.empty?
53
56
 
54
- rule.infilter(context, database, data)
57
+ rule.infilter(context, database, operation)
55
58
  end
56
59
 
57
60
  def extract_mongo_data operation
@@ -29,6 +29,9 @@ module Contrast
29
29
  write_marker = write?(action, *args)
30
30
  possible_write = write_marker && possible_write?(write_marker)
31
31
 
32
+ # Get the ia for current rule:
33
+ apply_classification(rule_name, Contrast::Agent::REQUEST_TRACKER.current)
34
+
32
35
  # Invoke semantic rules from here, not in a separate protect policy
33
36
  invoke_semantic_rules(path, possible_write, object, method)
34
37
  path_traversal_rule(path, possible_write, object, method)
@@ -29,6 +29,9 @@ module Contrast
29
29
  return if skip_analysis?
30
30
 
31
31
  sql = args[index]
32
+
33
+ # Get the ia for current rule:
34
+ apply_classification(rule_name, Contrast::Agent::REQUEST_TRACKER.current)
32
35
  rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, database, sql)
33
36
  rule.sub_rules.each { |sub_rule| sub_rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, sql) }
34
37
  end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/components/logger'
5
+ require 'contrast/agent/protect/input_analyzer/input_analyzer'
5
6
 
6
7
  module Contrast
7
8
  module Agent
@@ -44,6 +45,17 @@ module Contrast
44
45
  rule: rule_name)
45
46
  end
46
47
 
48
+ # applies input_analysis for the invoked rule
49
+ #
50
+ # @param rule_id [String] name of the rule
51
+ # @param context [Contrast::Agent::RequestContext] current request contest
52
+ def apply_classification rule_id, context
53
+ return unless context
54
+ return unless (ia = context.agent_input_analysis)
55
+
56
+ Contrast::Agent::Protect::InputAnalyzer.input_classification_for(rule_id, ia)
57
+ end
58
+
47
59
  protected
48
60
 
49
61
  # Calls the actual rule for this applicator, if required. Most rules
@@ -5,6 +5,7 @@ require 'contrast/components/logger'
5
5
  require 'contrast/components/scope'
6
6
  require 'contrast/utils/object_share'
7
7
  require 'contrast/agent/reporting/attack_result/response_type'
8
+ require 'contrast/agent/reporting/attack_result/attack_result'
8
9
 
9
10
  module Contrast
10
11
  module Agent
@@ -51,6 +52,23 @@ module Contrast
51
52
  Contrast::Utils::ObjectShare::EMPTY_ARRAY
52
53
  end
53
54
 
55
+ # The classification module used for each specific rule to
56
+ # classify input data and score it. Extend for each rule.
57
+ def classification; end
58
+
59
+ # Input Classification stage is done to determine if an user input is
60
+ # DEFINITEATTACK or to be ignored.
61
+ #
62
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
63
+ # @param value [Hash<String>] the value of the input.
64
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
65
+ # agent analysis from the current
66
+ # Request.
67
+ # @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
68
+ def classify input_type, value, input_analysis
69
+ classification.classify(rule_name, input_type, value, input_analysis)
70
+ end
71
+
54
72
  def enabled?
55
73
  # 1. it is not enabled because protect is not enabled
56
74
  return false unless ::Contrast::AGENT.enabled?
@@ -148,14 +166,14 @@ module Contrast
148
166
  # protect rule but did not exploit the application. As such, we need
149
167
  # to build a result to report this violation to TeamServer.
150
168
  #
151
- # @param context [Contrast::Agent::RequestContext] the context of the
169
+ # @param context [Contrast::Agent::RequestContext, nil] the context of the
152
170
  # request in which this input is evaluated.
153
171
  # @param ia_result [Contrast::Agent::Reporting::InputAnalysis] the
154
172
  # analysis of the input that was determined to be an attack
155
173
  # @param result [Contrast::Agent::Reporting::AttackResult, nil] previous
156
174
  # attack result for this rule, if one exists, in the case of
157
175
  # multiple inputs being found to violate the protection criteria
158
- # @param kwargs [Hash] key - value pairs of context individual rules
176
+ # @param kwargs [Hash, nil] key - value pairs of context individual rules
159
177
  # need to build out details to send to TeamServer to tell the
160
178
  # story of the attack
161
179
  # @return [Contrast::Agent::Reporting::AttackResult] the attack result from
@@ -288,11 +306,7 @@ module Contrast
288
306
  # @param _context [Contrast::Agent::RequestContext] the context of
289
307
  # the current request
290
308
  # @return [Contrast::Agent::Reporting::AttackResult]
291
- def build_attack_result _context
292
- result = Contrast::Agent::Reporting::AttackResult.new
293
- result.rule_id = rule_name
294
- result
295
- end
309
+ def build_attack_result _context; end
296
310
 
297
311
  # @param sample [Contrast::Agent::Reporting::RaspRuleSample]
298
312
  # @param result [Contrast::Agent::Reporting::AttackResult, nil] previous attack result for this rule, if one
@@ -96,6 +96,12 @@ module Contrast
96
96
  end
97
97
  end
98
98
 
99
+ def build_attack_result _context
100
+ result = Contrast::Agent::Reporting::AttackResult.new
101
+ result.rule_id = rule_name
102
+ result
103
+ end
104
+
99
105
  # @param context [Contrast::Agent::RequestContext]
100
106
  # @param potential_attack_string [String, nil]
101
107
  # @param **kwargs