contrast-agent 6.9.0 → 6.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -1
  4. data/lib/contrast/agent/middleware.rb +4 -2
  5. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +76 -83
  6. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +40 -35
  7. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +2 -0
  8. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +6 -3
  9. data/lib/contrast/agent/protect/policy/applies_path_traversal_rule.rb +3 -0
  10. data/lib/contrast/agent/protect/policy/applies_sqli_rule.rb +3 -0
  11. data/lib/contrast/agent/protect/policy/rule_applicator.rb +12 -0
  12. data/lib/contrast/agent/protect/rule/base.rb +19 -5
  13. data/lib/contrast/agent/protect/rule/base_service.rb +6 -0
  14. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +1 -1
  15. data/lib/contrast/agent/protect/rule/bot_blocker.rb +8 -0
  16. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +8 -0
  17. data/lib/contrast/agent/protect/rule/deserialization.rb +2 -2
  18. data/lib/contrast/agent/protect/rule/no_sqli.rb +24 -2
  19. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +1 -1
  20. data/lib/contrast/agent/protect/rule/path_traversal.rb +8 -0
  21. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +0 -1
  22. data/lib/contrast/agent/protect/rule/sqli.rb +6 -10
  23. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +6 -2
  24. data/lib/contrast/agent/protect/rule/unsafe_file_upload.rb +20 -0
  25. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +1 -1
  26. data/lib/contrast/agent/protect/rule/xss.rb +8 -0
  27. data/lib/contrast/agent/protect/rule/xxe.rb +2 -2
  28. data/lib/contrast/agent/protect/rule.rb +0 -3
  29. data/lib/contrast/agent/reporting/attack_result/user_input.rb +0 -1
  30. data/lib/contrast/agent/reporting/details/details.rb +0 -1
  31. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +12 -0
  32. data/lib/contrast/agent/reporting/report.rb +1 -0
  33. data/lib/contrast/agent/reporting/reporter.rb +11 -10
  34. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +4 -5
  35. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +13 -1
  36. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_activity.rb +20 -5
  37. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample.rb +0 -1
  38. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample_activity.rb +5 -0
  39. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +10 -1
  40. data/lib/contrast/agent/reporting/reporting_events/application_inventory.rb +2 -1
  41. data/lib/contrast/agent/reporting/reporting_events/application_reporting_event.rb +10 -0
  42. data/lib/contrast/agent/reporting/reporting_events/application_settings.rb +40 -0
  43. data/lib/contrast/agent/reporting/reporting_utilities/ng_response_extractor.rb +137 -0
  44. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +12 -4
  45. data/lib/contrast/agent/reporting/reporting_utilities/response_extractor.rb +100 -107
  46. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +5 -4
  47. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +97 -63
  48. data/lib/contrast/agent/reporting/reporting_workers/application_server_worker.rb +46 -0
  49. data/lib/contrast/agent/reporting/reporting_workers/reporter_heartbeat.rb +51 -0
  50. data/lib/contrast/agent/reporting/reporting_workers/reporting_workers.rb +14 -0
  51. data/lib/contrast/agent/reporting/reporting_workers/server_settings_worker.rb +46 -0
  52. data/lib/contrast/agent/reporting/settings/assess.rb +14 -1
  53. data/lib/contrast/agent/reporting/settings/assess_rule.rb +18 -0
  54. data/lib/contrast/agent/reporting/settings/helpers.rb +4 -2
  55. data/lib/contrast/agent/reporting/settings/protect.rb +17 -12
  56. data/lib/contrast/agent/reporting/settings/protect_rule.rb +18 -0
  57. data/lib/contrast/agent/reporting/settings/protect_server_feature.rb +1 -1
  58. data/lib/contrast/agent/reporting/settings/sensitive_data_masking.rb +1 -1
  59. data/lib/contrast/agent/reporting/settings/virtual_patch.rb +56 -0
  60. data/lib/contrast/agent/reporting/settings/virtual_patch_condition.rb +47 -0
  61. data/lib/contrast/agent/request_context_extend.rb +20 -0
  62. data/lib/contrast/agent/telemetry/base.rb +11 -10
  63. data/lib/contrast/agent/telemetry/events/exceptions/obfuscate.rb +108 -103
  64. data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +1 -1
  65. data/lib/contrast/agent/thread_watcher.rb +16 -10
  66. data/lib/contrast/agent/version.rb +1 -1
  67. data/lib/contrast/agent.rb +12 -0
  68. data/lib/contrast/agent_lib/api/init.rb +1 -7
  69. data/lib/contrast/agent_lib/api/input_tracing.rb +2 -4
  70. data/lib/contrast/agent_lib/interface.rb +1 -16
  71. data/lib/contrast/agent_lib/interface_base.rb +52 -39
  72. data/lib/contrast/agent_lib/return_types/eval_result.rb +2 -2
  73. data/lib/contrast/components/assess.rb +26 -4
  74. data/lib/contrast/components/polling.rb +4 -1
  75. data/lib/contrast/components/settings.rb +46 -3
  76. data/lib/contrast/config/config.rb +2 -2
  77. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  78. data/lib/contrast/config/protect_rules_configuration.rb +1 -1
  79. data/lib/contrast/extension/assess/array.rb +3 -3
  80. data/lib/contrast/extension/assess/regexp.rb +2 -2
  81. data/lib/contrast/logger/aliased_logging.rb +48 -15
  82. data/lib/contrast/utils/input_classification_base.rb +21 -4
  83. data/lib/contrast/utils/routes_sent.rb +2 -2
  84. data/lib/contrast/utils/telemetry.rb +1 -1
  85. data/lib/contrast/utils/telemetry_client.rb +1 -1
  86. data/resources/protect/policy.json +8 -0
  87. data/ruby-agent.gemspec +1 -1
  88. metadata +28 -18
  89. data/lib/contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification.rb +0 -96
  90. data/lib/contrast/agent/protect/rule/http_method_tampering.rb +0 -83
  91. data/lib/contrast/agent/reporting/details/http_method_tempering_details.rb +0 -27
  92. data/lib/contrast/agent/reporting/reporter_heartbeat.rb +0 -47
  93. data/lib/contrast/agent/reporting/server_settings_worker.rb +0 -44
  94. 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: 624b29e40ff797608bb6fef0ab00377cc1bc3e6756af01063d4768fad53bfbfa
4
+ data.tar.gz: 7380498d855e3f6b8a7387ee98351d118b6b70b7bc332536bf1124a870c1a48c
5
5
  SHA512:
6
- metadata.gz: ac0f4dcea0a62d6aa000659943f2b994a02940148fffdff2901ff4ff61d27fd257170ec4f48d0b1925d569dbc20de89a8866f76a17dd1c22aa15d9c80e4e9eb1
7
- data.tar.gz: bd2490f015d1a5c8be5f0e7a0fc60e5acdc436b03e32420cbf19542a53b760c843cd84a17a0e7a8a3794ec69e3aa909e63fabdb32f4c32d5daa48ece261176aa
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
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'contrast/agent/reporting/details/cmd_injection_details'
5
5
  require 'contrast/agent/reporting/attack_result/user_input'
6
+ require 'contrast/agent/protect/rule/cmdi/cmdi_input_classification'
6
7
 
7
8
  module Contrast
8
9
  module Agent
@@ -21,6 +22,13 @@ module Contrast
21
22
  MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
22
23
  ].cs__freeze
23
24
 
25
+ # CMDI input classification
26
+ #
27
+ # @return [module<Contrast::Agent::Protect::Rule::CmdiInputClassification>]
28
+ def classification
29
+ @_classification ||= Contrast::Agent::Protect::Rule::CmdiInputClassification.cs__freeze
30
+ end
31
+
24
32
  # CMDI infilter:
25
33
  #
26
34
  # @param context [Contrast::Agent::RequestContext] current request contest
@@ -1,7 +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/protect/rule/base'
4
+ require 'contrast/agent/protect/rule/base_service'
5
5
  require 'contrast/agent/reporting/details/untrusted_deserialization_details'
6
6
  require 'contrast/components/logger'
7
7
 
@@ -11,7 +11,7 @@ module Contrast
11
11
  module Rule
12
12
  # This class handles our implementation of the Untrusted
13
13
  # Deserialization Protect rule.
14
- class Deserialization < Contrast::Agent::Protect::Rule::Base
14
+ class Deserialization < Contrast::Agent::Protect::Rule::BaseService
15
15
  # The TeamServer recognized name of this rule
16
16
  include Contrast::Components::Logger::InstanceMethods
17
17
  # Used to name this input since input analysis isn't done for this
@@ -4,6 +4,7 @@
4
4
  require 'contrast/agent/protect/rule/base_service'
5
5
  require 'contrast/agent/protect/rule/sql_sample_builder'
6
6
  require 'contrast/agent/reporting/input_analysis/input_type'
7
+ require 'contrast/agent/protect/rule/no_sqli/no_sqli_input_classification'
7
8
 
8
9
  module Contrast
9
10
  module Agent
@@ -39,6 +40,13 @@ module Contrast
39
40
  APPLICABLE_USER_INPUTS
40
41
  end
41
42
 
43
+ # NoSQLI input classification
44
+ #
45
+ # @return [module<Contrast::Agent::Protect::Rule::NoSqliInputClassification>]
46
+ def classification
47
+ @_classification ||= Contrast::Agent::Protect::Rule::NoSqliInputClassification.cs__freeze
48
+ end
49
+
42
50
  # @raise [Contrast::SecurityException] if the attack is blocked
43
51
  # raised with BLOCK_MESSAGE
44
52
  def infilter context, database, query_string
@@ -78,9 +86,14 @@ module Contrast
78
86
 
79
87
  def find_attacker context, potential_attack_string, **kwargs
80
88
  if potential_attack_string
81
- # We need the query hash to be a JSON string to match on JSON input attacks
89
+ # We need the query hash to be a JSON string to match on JSON input attacks.
90
+ # Before that we need to check if a string is already in json form.
82
91
  begin
83
- potential_attack_string = JSON.generate(potential_attack_string).to_s
92
+ potential_attack_string = if json?(potential_attack_string)
93
+ potential_attack_string
94
+ else
95
+ JSON.generate(potential_attack_string).to_s
96
+ end
84
97
  rescue JSON::GeneratorError
85
98
  logger.trace('Error in JSON::generate', input: potential_attack_string)
86
99
  nil
@@ -88,6 +101,15 @@ module Contrast
88
101
  end
89
102
  super(context, potential_attack_string, **kwargs)
90
103
  end
104
+
105
+ # Check to see if a string is in JSON form.
106
+ #
107
+ # @return [Boolean]
108
+ def json? string
109
+ return true if JSON.parse(string)
110
+ rescue StandardError
111
+ false
112
+ end
91
113
  end
92
114
  end
93
115
  end