contrast-agent 6.8.0 → 6.10.0

Sign up to get free protection for your applications and to get access to all the features.
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