contrast-agent 6.9.0 → 6.10.0

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