contrast-agent 6.9.0 → 6.11.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 (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