contrast-agent 6.9.0 → 6.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/ext/build_funchook.rb +1 -1
  4. data/lib/contrast/agent/assess/policy/propagator/split.rb +1 -4
  5. data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -1
  6. data/lib/contrast/agent/middleware.rb +5 -3
  7. data/lib/contrast/agent/patching/policy/method_policy_extend.rb +6 -2
  8. data/lib/contrast/agent/patching/policy/trigger_node.rb +1 -1
  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 +40 -35
  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 +5 -2
  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 +19 -5
  17. data/lib/contrast/agent/protect/rule/base_service.rb +7 -2
  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 +8 -0
  20. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
  21. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +9 -1
  22. data/lib/contrast/agent/protect/rule/cmdi/cmdi_chained_command.rb +1 -1
  23. data/lib/contrast/agent/protect/rule/cmdi/cmdi_dangerous_path.rb +1 -1
  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 +8 -0
  28. data/lib/contrast/agent/protect/rule/sqli/postgres_sql_scanner.rb +0 -1
  29. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +0 -1
  30. data/lib/contrast/agent/protect/rule/sqli.rb +6 -10
  31. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +6 -2
  32. data/lib/contrast/agent/protect/rule/unsafe_file_upload.rb +20 -0
  33. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +1 -1
  34. data/lib/contrast/agent/protect/rule/xss.rb +8 -0
  35. data/lib/contrast/agent/protect/rule/xxe.rb +2 -2
  36. data/lib/contrast/agent/protect/rule.rb +0 -3
  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 +1 -0
  41. data/lib/contrast/agent/reporting/reporter.rb +12 -15
  42. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +4 -5
  43. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +13 -1
  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_reporting_event.rb +10 -0
  50. data/lib/contrast/agent/reporting/reporting_events/application_settings.rb +40 -0
  51. data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +9 -5
  52. data/lib/contrast/agent/reporting/reporting_events/observed_route.rb +8 -5
  53. data/lib/contrast/agent/reporting/reporting_utilities/endpoints.rb +7 -7
  54. data/lib/contrast/agent/reporting/reporting_utilities/ng_response_extractor.rb +137 -0
  55. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +12 -4
  56. data/lib/contrast/agent/reporting/reporting_utilities/response_extractor.rb +100 -107
  57. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +5 -4
  58. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +101 -67
  59. data/lib/contrast/agent/reporting/reporting_workers/application_server_worker.rb +46 -0
  60. data/lib/contrast/agent/reporting/reporting_workers/reporter_heartbeat.rb +51 -0
  61. data/lib/contrast/agent/reporting/reporting_workers/reporting_workers.rb +14 -0
  62. data/lib/contrast/agent/reporting/reporting_workers/server_settings_worker.rb +46 -0
  63. data/lib/contrast/agent/reporting/settings/assess.rb +14 -1
  64. data/lib/contrast/agent/reporting/settings/assess_rule.rb +18 -0
  65. data/lib/contrast/agent/reporting/settings/helpers.rb +4 -2
  66. data/lib/contrast/agent/reporting/settings/protect.rb +17 -12
  67. data/lib/contrast/agent/reporting/settings/protect_rule.rb +18 -0
  68. data/lib/contrast/agent/reporting/settings/protect_server_feature.rb +1 -1
  69. data/lib/contrast/agent/reporting/settings/sensitive_data_masking.rb +1 -1
  70. data/lib/contrast/agent/reporting/settings/virtual_patch.rb +56 -0
  71. data/lib/contrast/agent/reporting/settings/virtual_patch_condition.rb +47 -0
  72. data/lib/contrast/agent/request_context_extend.rb +20 -0
  73. data/lib/contrast/agent/telemetry/base.rb +13 -15
  74. data/lib/contrast/agent/telemetry/events/exceptions/obfuscate.rb +108 -103
  75. data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +1 -1
  76. data/lib/contrast/agent/thread_watcher.rb +16 -10
  77. data/lib/contrast/agent/version.rb +1 -1
  78. data/lib/contrast/agent.rb +12 -0
  79. data/lib/contrast/agent_lib/api/init.rb +1 -7
  80. data/lib/contrast/agent_lib/api/input_tracing.rb +2 -4
  81. data/lib/contrast/agent_lib/interface.rb +1 -16
  82. data/lib/contrast/agent_lib/interface_base.rb +52 -39
  83. data/lib/contrast/agent_lib/return_types/eval_result.rb +2 -2
  84. data/lib/contrast/components/assess.rb +26 -4
  85. data/lib/contrast/components/config.rb +1 -1
  86. data/lib/contrast/components/polling.rb +4 -1
  87. data/lib/contrast/components/settings.rb +46 -3
  88. data/lib/contrast/config/config.rb +2 -2
  89. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  90. data/lib/contrast/config/protect_rules_configuration.rb +1 -1
  91. data/lib/contrast/extension/assess/array.rb +3 -3
  92. data/lib/contrast/extension/assess/regexp.rb +2 -2
  93. data/lib/contrast/framework/rack/patch/session_cookie.rb +2 -1
  94. data/lib/contrast/logger/aliased_logging.rb +48 -15
  95. data/lib/contrast/utils/duck_utils.rb +18 -0
  96. data/lib/contrast/utils/heap_dump_util.rb +1 -1
  97. data/lib/contrast/utils/input_classification_base.rb +21 -4
  98. data/lib/contrast/utils/log_utils.rb +1 -1
  99. data/lib/contrast/utils/middleware_utils.rb +1 -1
  100. data/lib/contrast/utils/patching/policy/patch_utils.rb +2 -2
  101. data/lib/contrast/utils/routes_sent.rb +6 -2
  102. data/lib/contrast/utils/telemetry.rb +2 -2
  103. data/lib/contrast/utils/telemetry_client.rb +1 -1
  104. data/resources/protect/policy.json +8 -0
  105. data/ruby-agent.gemspec +6 -6
  106. metadata +40 -30
  107. data/lib/contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification.rb +0 -96
  108. data/lib/contrast/agent/protect/rule/http_method_tampering.rb +0 -83
  109. data/lib/contrast/agent/reporting/details/http_method_tempering_details.rb +0 -27
  110. data/lib/contrast/agent/reporting/reporter_heartbeat.rb +0 -47
  111. data/lib/contrast/agent/reporting/server_settings_worker.rb +0 -44
  112. 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: b850f63bce180f09f998f5363f58b01e9c69db61a2f98a31db97fb59b82564d7
4
- data.tar.gz: 811072666998fb4daf0f49d2514d875b45c18d79d29398639862f1c2144aa930
3
+ metadata.gz: fddcf57afedf79d84cacef08c959a190f29f6db9248d34d1a01c3abe1b13553b
4
+ data.tar.gz: 599882cd812552c2cc12c86f6ecd526e7a7d1e458cf8ea4d961d17ef5179c9ad
5
5
  SHA512:
6
- metadata.gz: ac0f4dcea0a62d6aa000659943f2b994a02940148fffdff2901ff4ff61d27fd257170ec4f48d0b1925d569dbc20de89a8866f76a17dd1c22aa15d9c80e4e9eb1
7
- data.tar.gz: bd2490f015d1a5c8be5f0e7a0fc60e5acdc436b03e32420cbf19542a53b760c843cd84a17a0e7a8a3794ec69e3aa909e63fabdb32f4c32d5daa48ece261176aa
6
+ metadata.gz: e91dd42c9aa9f9cb934bfe878e0f6be951f9397978c874dd2a4b38c0a78f1cfe6b7e957ec6abaa3bb97a194f6494950f9ec93ebad408cf2767df06b1475bd373
7
+ data.tar.gz: ad17804c1abf24410ba2bbe7798ce142eba284cb0911830e40f6f92917a47961d4cfaefa79ed8affb8d1d821e27a85f57ace126a64417f6fe672f965916c3dba
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
@@ -17,7 +17,7 @@ unless find_header('funchook.h', ext_path)
17
17
  extension_paths = Dir[contrast_gem_dir_search]
18
18
  extension_paths.map! do |extension_path|
19
19
  target_path = File.join(extension_path, 'shared_libraries')
20
- FileUtils.mkdir_p(target_path) unless File.exist?(target_path)
20
+ FileUtils.mkdir_p(target_path)
21
21
  target_path
22
22
  end
23
23
  bundler_install_target_paths += extension_paths
@@ -17,12 +17,11 @@ module Contrast
17
17
  module Propagator
18
18
  # This class is specifically for String#split & String#grapheme_clusters propagation
19
19
  # it propagates tag ranges from a string to elements within an untracked array
20
- class Split < Contrast::Agent::Assess::Policy::Propagator::Base
20
+ class Split < Contrast::Agent::Assess::Policy::Propagator::Base # rubocop:disable Metrics/ClassLength
21
21
  extend Contrast::Components::Scope::InstanceMethods
22
22
  extend Contrast::Components::Logger::InstanceMethods
23
23
  extend Contrast::Utils::Assess::SplitUtils
24
24
  extend Contrast::Utils::Assess::EventLimitUtils
25
-
26
25
  SPLIT_TRACKER = Contrast::Utils::ThreadTracker.new
27
26
 
28
27
  class << self
@@ -61,7 +60,6 @@ module Contrast
61
60
  rescue Exception => e # rubocop:disable Lint/RescueException
62
61
  logger.warn('Unable to record split context', e)
63
62
  end
64
-
65
63
  yield
66
64
  ensure
67
65
  # String#split exit. Remove propagation context.
@@ -158,7 +156,6 @@ module Contrast
158
156
  # Get tags for element from source by element range.
159
157
  range = current_index...(current_index + target_elem.length)
160
158
  tags = source_properties.tags_at_range(range)
161
-
162
159
  # Set element properties accordingly.
163
160
  elem_properties.clear_tags
164
161
  tags.each_pair { |key, value| elem_properties.set_tags(key, value) }
@@ -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
@@ -14,6 +14,7 @@ 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'
@@ -23,7 +24,7 @@ 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
@@ -43,7 +44,7 @@ module Contrast
43
44
  @app = app # THIS MUST BE FIRST AND ALWAYS SET!
44
45
  setup_agent # THIS MUST BE SECOND AND ALWAYS CALLED!
45
46
  unless ::Contrast::AGENT.enabled?
46
- logger.error('The Agent was unable to initialize before the application middleware was initialized. '\
47
+ logger.error('The Agent was unable to initialize before the application middleware was initialized. ' \
47
48
  'Disabling permanently.')
48
49
  ::Contrast::AGENT.disable! # ensure the agent is disabled (probably redundant)
49
50
  return
@@ -171,12 +172,13 @@ module Contrast
171
172
  with_contrast_scope do
172
173
  context.extract_after(response) # update context with final response information
173
174
 
174
- # Build and report all collected findings prior response
175
175
  Contrast::Agent::FINDINGS.report_collected_findings unless Contrast::Agent::FINDINGS.collection.empty?
176
176
  # All protect rules, which are trigger but require response to be reported
177
177
  Contrast::Agent::EXPLOITS.report_recorded_exploits(context) unless Contrast::Agent::EXPLOITS.collection.empty?
178
178
  # Process Worth Watching Inputs for v2 rules
179
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
180
182
 
181
183
  if Contrast::Agent.framework_manager.streaming?(env)
182
184
  context.reset_activity
@@ -57,8 +57,12 @@ module Contrast
57
57
  nodes.find { |node| node }&.method_visibility
58
58
  end
59
59
 
60
- def check_method_policy_nodes_empty?(source_node, propagation_node, trigger_node, protect_node,
61
- inventory_node, deadzone_node)
60
+ def check_method_policy_nodes_empty?(source_node,
61
+ propagation_node,
62
+ trigger_node,
63
+ protect_node,
64
+ inventory_node,
65
+ deadzone_node)
62
66
  return false unless source_node.nil? && propagation_node.nil? && trigger_node.nil? && protect_node.nil? &&
63
67
  inventory_node.nil? && deadzone_node.nil?
64
68
 
@@ -46,7 +46,7 @@ module Contrast
46
46
  super
47
47
  unless applicator.public_methods(false).any?(applicator_method)
48
48
  raise(ArgumentError,
49
- "#{ id } did not have a proper applicator method: "\
49
+ "#{ id } did not have a proper applicator method: " \
50
50
  "#{ applicator } does not respond to #{ applicator_method }. Unable to create.")
51
51
  end
52
52
  validate_properties
@@ -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
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/agent/worker_thread'
5
+ require 'contrast/agent/reporting/input_analysis/input_analysis_result'
5
6
  require 'contrast/agent/reporting/input_analysis/score_level'
6
7
  require 'contrast/agent/reporting/reporting_events/application_activity'
7
8
  require 'contrast/utils/input_classification_base'
@@ -28,69 +29,73 @@ module Contrast
28
29
  return if running?
29
30
 
30
31
  @_thread = Contrast::Agent::Thread.new do
31
- logger.info('Starting Worth Watching Analyzer thread.')
32
+ logger.info('[WorthWatchingAnalyzer] Starting thread.')
32
33
  loop do
33
34
  sleep(REPORT_INTERVAL_SECOND)
34
35
  next if queue.empty?
35
36
 
36
37
  report = false
37
- num_to_report = queue.length
38
+ # build attack_results for all infilter active protect rules.
39
+ results = build_results(queue.pop)
38
40
  activity = Contrast::Agent::Reporting::ApplicationActivity.new
39
- num_to_report.times do
40
- next unless (attack_result = eval_input(queue.pop))
41
+ results.each do |result|
42
+ next unless (attack_result = eval_input(result))
41
43
 
42
44
  activity.attach_defend(attack_result)
43
45
  report = true
44
46
  end
45
47
  Contrast::Agent.reporter.send_event_immediately(activity) if report
46
48
  rescue StandardError => e
47
- logger.debug('Worth Watching Analyzer thread could not process result because of:', e)
49
+ logger.debug('[WorthWatchingAnalyzer] thread could not process result because of:', e)
48
50
  end
49
51
  end
50
52
  end
51
53
 
52
- # param Contrast::Agent::Reporting::InputAnalysis
54
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
53
55
  def add_to_queue input_analysis
54
- return if input_analysis&.results&.empty?
56
+ return unless input_analysis
55
57
 
56
58
  if queue.size >= QUEUE_SIZE
57
- logger.debug('WorthWatching queue at max size, skip input_result')
59
+ logger.debug('[WorthWatchingAnalyzer] queue at max size, skip input_result')
58
60
  return
59
61
  end
60
- input_analysis.results.select { |val| val.score_level == WORTHWATCHING }.
61
- each do |ia_result|
62
- queue << ia_result
63
- 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
64
67
  end
65
68
 
66
69
  private
67
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
+
68
91
  # @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the WorthWatching InputAnalysisResult
69
- # @return [Contrast::Agent::Reporting::InputAnalysisResult, nil] InputAnalysisResult updated Result or nil
92
+ # @return [Contrast::Agent::Reporting::AttackResult, nil] InputAnalysisResult updated Result or nil
70
93
  def eval_input ia_result
71
- return if ia_result.nil?
72
-
73
- if ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD
74
- logger.debug("Skip anaylsis: Input size is larger than #{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
75
- return
76
- end
94
+ return build_attack_result(ia_result) unless ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD
77
95
 
78
- begin
79
- input_eval = Timeout.timeout(AGENTLIB_TIMEOUT) do
80
- Contrast::AGENT_LIB.eval_input(ia_result.value,
81
- convert_input_type(ia_result.input_type),
82
- Contrast::AGENT_LIB.rule_set[ia_result.rule_id],
83
- Contrast::AGENT_LIB.eval_option[:NONE])
84
- end
85
- score = input_eval&.score || 0
86
- return if score <= THRESHOLD
87
-
88
- ia_result.score_level = DEFINITEATTACK
89
- build_attack_result(ia_result)
90
- rescue Timeout::Error => e
91
- logger.warn('AgentLib timed out when processing WORTHWATCHING InputAnalysisResult', e, ia_result)
92
- nil
93
- end
96
+ logger.debug("[WorthWatchingAnalyzer] Skipping analysis: Input size is larger than
97
+ #{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
98
+ nil
94
99
  end
95
100
 
96
101
  # @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the updated InputAnalysisResult
@@ -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)
@@ -98,10 +101,10 @@ module Contrast
98
101
  CS__SAFER_REL_PATHS = %w[public app log tmp].cs__freeze
99
102
  def safer_abs_paths
100
103
  @_safer_abs_paths ||= begin
101
- pwd = ENV['PWD']
104
+ pwd = ENV.fetch('PWD', nil)
102
105
  if pwd
103
106
  tmp = CS__SAFER_REL_PATHS.map { |r| "#{ pwd }/#{ r }" }
104
- gems = ENV['GEM_PATH']
107
+ gems = ENV.fetch('GEM_PATH', nil)
105
108
  tmp += gems.split(Contrast::Utils::ObjectShare::COLON) if gems
106
109
  tmp.map!(&:downcase)
107
110
  tmp
@@ -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?
@@ -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
@@ -91,11 +91,16 @@ module Contrast
91
91
  return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless context&.agent_input_analysis&.results
92
92
 
93
93
  context.agent_input_analysis.results.select do |ia_result|
94
- ia_result.rule_id == rule_name &&
95
- ia_result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE
94
+ ia_result.rule_id == rule_name && ia_result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE
96
95
  end
97
96
  end
98
97
 
98
+ def build_attack_result _context
99
+ result = Contrast::Agent::Reporting::AttackResult.new
100
+ result.rule_id = rule_name
101
+ result
102
+ end
103
+
99
104
  # @param context [Contrast::Agent::RequestContext]
100
105
  # @param potential_attack_string [String, nil]
101
106
  # @param **kwargs
@@ -39,7 +39,7 @@ module Contrast
39
39
 
40
40
  value.each_value do |val|
41
41
  result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, val)
42
- input_analysis.results << result if result
42
+ append_result(input_analysis, result)
43
43
  end
44
44
 
45
45
  input_analysis
@@ -6,6 +6,7 @@ require 'contrast/components/logger'
6
6
  require 'contrast/agent/reporting/input_analysis/input_type'
7
7
  require 'contrast/agent/reporting/input_analysis/score_level'
8
8
  require 'contrast/agent_lib/interface'
9
+ require 'contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification'
9
10
 
10
11
  module Contrast
11
12
  module Agent
@@ -27,6 +28,13 @@ module Contrast
27
28
  APPLICABLE_USER_INPUTS
28
29
  end
29
30
 
31
+ # Bot blocker input classification
32
+ #
33
+ # @return [module<Contrast::Agent::Protect::Rule::BotBlockerInputClassification>]
34
+ def classification
35
+ @_classification ||= Contrast::Agent::Protect::Rule::BotBlockerInputClassification.cs__freeze
36
+ end
37
+
30
38
  # BotBlocker prefilter:
31
39
  #
32
40
  # @param context [Contrast::Agent::RequestContext] current request contest