contrast-agent 7.4.0 → 7.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent/hooks/at_exit_hook.rb +16 -1
  3. data/lib/contrast/agent/middleware/middleware.rb +1 -1
  4. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +19 -12
  5. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +55 -20
  6. data/lib/contrast/agent/protect/policy/rule_applicator.rb +1 -4
  7. data/lib/contrast/agent/protect/rule/base.rb +56 -25
  8. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +12 -4
  9. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +0 -26
  10. data/lib/contrast/agent/protect/rule/cmdi/cmd_injection.rb +2 -5
  11. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +2 -4
  12. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +2 -1
  13. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +4 -4
  14. data/lib/contrast/agent/protect/rule/input_classification/base.rb +1 -4
  15. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +34 -2
  16. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli.rb +5 -2
  17. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal.rb +12 -7
  18. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +2 -2
  19. data/lib/contrast/agent/protect/rule/sqli/sqli_base_rule.rb +2 -3
  20. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +3 -4
  21. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload.rb +3 -0
  22. data/lib/contrast/agent/protect/rule/utils/builders.rb +3 -4
  23. data/lib/contrast/agent/protect/rule/utils/filters.rb +32 -16
  24. data/lib/contrast/agent/protect/rule/xss/xss.rb +80 -0
  25. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +9 -2
  26. data/lib/contrast/agent/reporting/details/xss_match.rb +17 -0
  27. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +32 -0
  28. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +4 -34
  29. data/lib/contrast/agent/reporting/reporting_events/finding.rb +1 -5
  30. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +2 -5
  31. data/lib/contrast/agent/reporting/reporting_utilities/build_preflight.rb +4 -4
  32. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +5 -1
  33. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +1 -1
  34. data/lib/contrast/agent/request/request_context_extend.rb +0 -2
  35. data/lib/contrast/agent/version.rb +1 -1
  36. data/lib/contrast/components/assess.rb +4 -0
  37. data/lib/contrast/framework/rails/support.rb +2 -2
  38. data/lib/contrast/logger/cef_log.rb +30 -4
  39. data/lib/contrast/utils/io_util.rb +3 -0
  40. data/lib/contrast/utils/json.rb +1 -1
  41. data/lib/contrast/utils/log_utils.rb +21 -10
  42. data/ruby-agent.gemspec +3 -2
  43. metadata +18 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1266effb11fb78123e8461ce7295f9b4a67a88b7dda632bc57da5d70cb39f87d
4
- data.tar.gz: e553aa33bd73ab712585b774578c10ff6f19ae925fdea8adb89c3b6ac253e399
3
+ metadata.gz: f6b34ada48cf9e91e05d85ea0f003575119b751bf135ef49709fb1501c475b3e
4
+ data.tar.gz: 2915fb037c832fec213dfc7835b8d732e1c22005987ef2c4287dfb55264d41a2
5
5
  SHA512:
6
- metadata.gz: 7abce257d4092010babc21cff242307343ea2fc83bdbd040b8cd95e64be9b2e640963a06ae017053989be17e98442bbc528987c2da5b8f7bc8688b776022c783
7
- data.tar.gz: f85de50b08e0eaa5f5d8c6a571fe4d04a03cbbcaab1f9c7f2431dac6b4373b00e04f63be94b5f02d56e222248c5fe256b61fb43ba123d041e1ce585b80e63a5f
6
+ metadata.gz: 627d1959c5993100f6bc08624d1a68627b02af18f6aa48c074f0cf374925877761b09077ce21366b6d5ddafb3185907472564753b3fbab65f6f7d74d2b74dc10
7
+ data.tar.gz: 224228b9e9c276c39e6d78a330a456b37cf550b4155c4b96f5ff1d29e7bf7846521c102f510061f8d6ec5e9245b99fe567cce7a5202dd2acfd62c6e7bb9ca795
@@ -27,7 +27,8 @@ module Contrast
27
27
  pp_id: @ppid,
28
28
  process_pid: Process.pid,
29
29
  process_pp_id: Process.ppid)
30
-
30
+ $stdout.puts('[Contrast Agent] Graceful shutdown...')
31
+ report_traces
31
32
  context = Contrast::Agent::REQUEST_TRACKER.current
32
33
  return unless context
33
34
 
@@ -39,6 +40,20 @@ module Contrast
39
40
  end
40
41
  Contrast::Agent.reporter&.send_event_immediately(context.activity)
41
42
  end
43
+
44
+ def self.report_traces
45
+ return unless Contrast::ASSESS.enabled?
46
+
47
+ collection = Contrast::Agent::Reporting::ReportingStorage.collection
48
+
49
+ # report gathered traces:
50
+ return if collection.empty?
51
+
52
+ collection.each do |_id, finding|
53
+ preflight = Contrast::Agent::Reporting::BuildPreflight.generate(finding)
54
+ Contrast::Agent.reporter&.send_event_immediately(preflight)
55
+ end
56
+ end
42
57
  end
43
58
  end
44
59
  end
@@ -191,7 +191,7 @@ module Contrast
191
191
  # Now we can build the ia_results only for postfilter rules.
192
192
  context.protect_postfilter_ia
193
193
  # Process Worth Watching Inputs for v2 rules
194
- Contrast::Agent.worth_watching_analyzer&.add_to_queue(context.agent_input_analysis)
194
+ Contrast::Agent.worth_watching_analyzer&.add_to_queue(context)
195
195
 
196
196
  if Contrast::Agent.framework_manager.streaming?(env)
197
197
  context.reset_activity
@@ -20,6 +20,7 @@ require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
20
20
  require 'contrast/agent/protect/rule/xss/xss'
21
21
  require 'contrast/components/logger'
22
22
  require 'contrast/utils/object_share'
23
+ require 'contrast/utils/duck_utils'
23
24
  require 'contrast/agent/protect/rule/input_classification/base64_statistic'
24
25
  require 'json'
25
26
 
@@ -33,10 +34,10 @@ module Contrast
33
34
  DISPOSITION_FILENAME = 'filename'
34
35
  PREFILTER_RULES = %w[bot-blocker unsafe-file-upload reflected-xss].cs__freeze
35
36
  INFILTER_RULES = %w[
36
- sql-injection cmd-injection reflected-xss bot-blocker unsafe-file-upload path-traversal
37
+ sql-injection cmd-injection bot-blocker unsafe-file-upload path-traversal
37
38
  nosql-injection
38
39
  ].cs__freeze
39
- POSTFILTER_RULES = %w[sql-injection cmd-injection reflected-xss path-traversal nosql-injection].cs__freeze
40
+ POSTFILTER_RULES = %w[sql-injection cmd-injection path-traversal nosql-injection].cs__freeze
40
41
  AGENTLIB_TIMEOUT = 5.cs__freeze
41
42
  TIMEOUT_ERROR_MESSAGE = '[AgentLib] Timed out when processing InputAnalysisResult'
42
43
  STANDARD_ERROR_MESSAGE = '[InputAnalyzer] Exception raise while doing input analysis:'
@@ -100,12 +101,15 @@ module Contrast
100
101
  # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from analyze method.
101
102
  # @param interval [Integer] The timeout determined for the AgentLib analysis to be performed.
102
103
  def input_classification_for rule_id, input_analysis, interval: AGENTLIB_TIMEOUT
103
- return unless input_analysis&.inputs
104
+ return if input_analysis.analysed_rules.include?(rule_id)
105
+ return if input_analysis.no_inputs?
104
106
  return unless (protect_rule = Contrast::PROTECT.rule(rule_id)) && protect_rule.enabled?
105
107
 
106
108
  input_analysis.inputs.each do |input_type, value|
107
- next if value.nil? || value.empty?
109
+ value = handle_header(input_type, value)
110
+ next if Contrast::Utils::DuckUtils.empty_duck?(value)
108
111
 
112
+ # Traverse only the Header values:
109
113
  Timeout.timeout(interval) do
110
114
  protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
111
115
  end
@@ -128,14 +132,12 @@ module Contrast
128
132
  # for each protect rule.
129
133
  # @param prefilter [Boolean] flag to set input analysis for prefilter rules only
130
134
  # @param postfilter [Boolean] flag to set input analysis for postfilter rules.
131
- # @param infilter [Boolean]
132
135
  # @param interval [Integer] The timeout determined for the AgentLib analysis to be performed
133
136
  # @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
134
137
  # @raise [Timeout::Error] If timeout is met.
135
138
  def input_classification(input_analysis,
136
139
  prefilter: false,
137
140
  postfilter: false,
138
- infilter: false,
139
141
  interval: AGENTLIB_TIMEOUT)
140
142
  return unless input_analysis
141
143
 
@@ -147,17 +149,22 @@ module Contrast
147
149
  INFILTER_RULES
148
150
  end
149
151
 
150
- rules.each do |rule_id|
151
- # Check to see if rules is already triggered only for infilter:
152
- next if input_analysis.triggered_rules.include?(rule_id) && infilter
153
-
154
- input_classification_for(rule_id, input_analysis, interval: interval)
155
- end
152
+ rules.each { |rule_id| input_classification_for(rule_id, input_analysis, interval: interval) }
156
153
  input_analysis
157
154
  end
158
155
 
159
156
  private
160
157
 
158
+ # Extracts header values, and skips keys if input_type is Header.
159
+ # @param input_type [Symbol]
160
+ # @param value [Hash, nil]
161
+ # @return value [Hash, nil]
162
+ def handle_header input_type, value
163
+ return value&.values || [] if input_type == Contrast::Agent::Reporting::InputType::HEADER
164
+
165
+ value
166
+ end
167
+
161
168
  # Extract the filename and name of the Content Disposition Header.
162
169
  #
163
170
  # @param inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
@@ -42,16 +42,15 @@ module Contrast
42
42
  next if queue.empty?
43
43
 
44
44
  report = false
45
- # build attack_results for all infilter active protect rules.
46
- stored_ia = queue.pop
47
- results = build_results(stored_ia)
48
- activity = Contrast::Agent::Reporting::ApplicationActivity.new(ia_request: stored_ia.request)
45
+ stored_context, stored_ia, results, activity = extract_from_context
46
+
49
47
  results.each do |result|
50
- next unless (attack_result = eval_input(result))
48
+ next unless (attack_result = eval_input(stored_context, result, stored_ia))
51
49
 
52
50
  activity.attach_defend(attack_result)
53
51
  report = true
54
52
  end
53
+
55
54
  report_activity(activity) if report
56
55
  # Handle reporting of IA Cache statistics:
57
56
  enqueue_cache_event(stored_ia.request)
@@ -62,9 +61,22 @@ module Contrast
62
61
  end
63
62
  end
64
63
 
65
- # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
66
- def add_to_queue input_analysis
67
- return unless input_analysis
64
+ # build attack_results for all infilter active protect rules.
65
+ # Stored Context will update the logger context and build attack results for protect rules.
66
+ # Note: call only in thread loop as it extracts from the queue.
67
+ #
68
+ # @return [Array<stored_context, stored_ia, results, activity>]
69
+ def extract_from_context
70
+ stored_context = queue.pop
71
+ stored_ia = stored_context.agent_input_analysis
72
+ results = build_results(stored_ia)
73
+ activity = Contrast::Agent::Reporting::ApplicationActivity.new(ia_request: stored_ia.request)
74
+ [stored_context, stored_ia, results, activity]
75
+ end
76
+
77
+ # @param context [Contrast::Agent::RequestContext]
78
+ def add_to_queue context
79
+ return unless context
68
80
 
69
81
  if queue.size >= QUEUE_SIZE
70
82
  logger.debug('[WorthWatchingAnalyzer] queue at max size, skip input_result')
@@ -74,7 +86,7 @@ module Contrast
74
86
  # we need to save the ia which contains the request and saved extracted user inputs to
75
87
  # be evaluated on the thread rather than building results here. This way we allow the
76
88
  # request to continue and will build the attack results later.
77
- queue << input_analysis.dup
89
+ queue << context.dup
78
90
  end
79
91
 
80
92
  private
@@ -91,6 +103,9 @@ module Contrast
91
103
  Contrast::Agent::Protect::InputAnalyzer.lru_cache.clear_statistics
92
104
  end
93
105
 
106
+ # Enqueue for Telemetry reporting all base64 related events.
107
+ #
108
+ # @param request [Contrast::Agent::Request] stored request.
94
109
  def enqueue_encoding_event request
95
110
  return unless Contrast::Agent::Telemetry::Base.enabled?
96
111
  return unless Contrast::PROTECT.normalize_base64?
@@ -102,16 +117,16 @@ module Contrast
102
117
 
103
118
  # This method will build the attack results from the saved ia.
104
119
  #
105
- # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
120
+ # @param stored_ia [Contrast::Agent::Reporting::InputAnalysis]
106
121
  # @return attack_results [array<Contrast::Agent::Reporting::InputAnalysisResult>] all the results
107
122
  # from the input analysis.
108
- def build_results input_analysis
123
+ def build_results stored_ia
109
124
  # Construct the input analysis for the all the infilter rules that were not triggered.
110
125
  # There is a set timeout for each rule to be analyzed in. The infilter flag will make
111
126
  # sure that if a rule is already triggered during the infilter phase it will not be analyzed
112
127
  # now, making sure we don't report same rule twice.
113
- Contrast::Agent::Protect::InputAnalyzer.input_classification(input_analysis, infilter: true)
114
- results = input_analysis.results.reject do |val|
128
+ Contrast::Agent::Protect::InputAnalyzer.input_classification(stored_ia)
129
+ results = stored_ia.results.reject do |val|
115
130
  val.score_level == Contrast::Agent::Reporting::InputAnalysisResult::SCORE_LEVEL::IGNORE
116
131
  end
117
132
  return results if results
@@ -119,39 +134,59 @@ module Contrast
119
134
  []
120
135
  end
121
136
 
137
+ # Evaluates the stored ia results and builds attack results if any.
138
+ #
139
+ # @param stored_context [Contrast::Agent::RequestContext]
122
140
  # @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the WorthWatching InputAnalysisResult
141
+ # @param stored_ia [Contrast::Agent::Reporting::InputAnalysis] the stored InputAnalysis
123
142
  # @return [Contrast::Agent::Reporting::AttackResult, nil] InputAnalysisResult updated Result or nil
124
- def eval_input ia_result
125
- return build_attack_result(ia_result) unless ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD
143
+ def eval_input stored_context, ia_result, stored_ia
144
+ return skip_log if ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD
126
145
 
127
- logger.debug("[WorthWatchingAnalyzer] Skipping analysis: Input size is larger than
128
- #{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
129
- nil
146
+ build_attack_result(stored_context, ia_result, stored_ia)
130
147
  end
131
148
 
149
+ # Creates new Attack Event per rule that will be triggered or probed.
150
+ #
151
+ # @param stored_context [Contrast::Agent::RequestContext]
132
152
  # @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the updated InputAnalysisResult
133
153
  # with a score of :DEFINITEATTACK
154
+ # @param stored_ia [Contrast::Agent::Reporting::InputAnalysis] the stored InputAnalysis
134
155
  # @return [Contrast::Agent::Reporting::AttackResult] the attack result from
135
156
  # this input
136
- def build_attack_result ia_result
137
- Contrast::PROTECT.rule(ia_result.rule_id).build_attack_without_match(nil, ia_result, nil)
157
+ def build_attack_result stored_context, ia_result, stored_ia
158
+ return if stored_ia.triggered_rules.include?(ia_result.rule_id)
159
+
160
+ Contrast::PROTECT.rule(ia_result.rule_id).build_attack_without_match(stored_context, ia_result, nil)
138
161
  end
139
162
 
163
+ # @return [Queue]
140
164
  def queue
141
165
  @_queue ||= Queue.new
142
166
  end
143
167
 
168
+ # Reports all gather activities to batch.
169
+ #
170
+ # @param activity [Contrast::Agent::Reporting::ApplicationActivity]
144
171
  def report_activity activity
145
172
  logger.debug('[WorthWatchingAnalyzer] preparing to send activity batch')
146
173
  add_activity_to_batch(activity)
147
174
  report_batch
148
175
  end
149
176
 
177
+ # Deletes Queue and closes it.
150
178
  def delete_queue!
151
179
  @_queue&.clear
152
180
  @_queue&.close
153
181
  @_queue = nil
154
182
  end
183
+
184
+ # Logs a message that the input was skipped because it was too large.
185
+ def skip_log
186
+ logger.debug("[WorthWatchingAnalyzer] Skipping analysis: Input size is larger than
187
+ #{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
188
+ nil
189
+ end
155
190
  end
156
191
  end
157
192
  end
@@ -55,10 +55,7 @@ module Contrast
55
55
  return unless (ia = context.agent_input_analysis)
56
56
 
57
57
  Contrast::Agent::Protect::InputAnalyzer.input_classification_for(rule_id, ia)
58
- # We add the triggered rule to the list. After request analysis will skip this rule
59
- # as already it's input applicable types has been analysed.
60
- ia.triggered_rules << rule_name
61
- ia
58
+ context.agent_input_analysis.record_analysed_rule(rule_id)
62
59
  end
63
60
 
64
61
  protected
@@ -141,20 +141,30 @@ module Contrast
141
141
  end
142
142
 
143
143
  # With this we log to CEF
144
- #
145
- # @param result [Contrast::Agent::Reporting::AttackResult]
144
+ # @param result [Contrast::Agent::Reporting::InputAnalysisResult]
146
145
  # @param attack [Symbol] the type of message we want to send
147
146
  # @param value [String] the input value we want to log
148
- def cef_logging result, attack = :ineffective_attack, value: nil
147
+ # @param input_type [String] the input type we want to log
148
+ # @param context [Contrast::Agent::RequestContext]
149
+ def cef_logging result, attack = :ineffective_attack, value: nil, input_type: nil, context: nil
149
150
  sample = result.samples[0]
150
151
  outcome = result.response.to_s
151
- input_type = sample.user_input.input_type.to_s
152
- input_value = sample.user_input.value || value
153
- cef_logger.send(attack, result.rule_id, outcome, input_type, input_value)
152
+ input_type = sample&.user_input&.input_type&.to_s || input_type
153
+ input_value = sample&.user_input&.value || value
154
+ cef_logger.send(attack, result.rule_id, outcome, input_type, input_value, context)
154
155
  end
155
156
 
156
157
  protected
157
158
 
159
+ # Records the rule being triggered at sink.
160
+ #
161
+ # @param context [Contrast::Agent::RequestContext]
162
+ def record_triggered context
163
+ return unless context
164
+
165
+ context.agent_input_analysis.record_rule_triggered(rule_name)
166
+ end
167
+
158
168
  # Assign the mode from active settings.
159
169
  #
160
170
  # @return mode [Symbol]
@@ -191,22 +201,20 @@ module Contrast
191
201
  # @param potential_attack_string [String, nil]
192
202
  # @param ia_results [Array<Contrast::Agent::Reporting::InputAnalysis>]
193
203
  # @param **kwargs
194
- # @return [Contrast::Agent::Reporting, nil]
204
+ # @return [Contrast::Agent::Reporting::AttackResult, nil]
195
205
  def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs
196
206
  logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string)
207
+ return unless ia_results&.any?
208
+ return build_attack_without_match(context, ia_results[0], nil, **kwargs) unless potential_attack_string
197
209
 
198
- result = nil
199
210
  ia_results.each do |ia_result|
200
- if potential_attack_string
201
- idx = potential_attack_string.index(ia_result.value)
202
- next unless idx
203
-
204
- result = build_attack_with_match(context, ia_result, result, potential_attack_string, **kwargs)
205
- else
206
- result = build_attack_without_match(context, ia_result, result, **kwargs)
207
- end
211
+ idx = potential_attack_string.index(ia_result.value)
212
+ next unless idx
213
+
214
+ result = build_attack_with_match(context, ia_result, result || nil, potential_attack_string, **kwargs)
215
+ return result if result
208
216
  end
209
- result
217
+ nil
210
218
  end
211
219
 
212
220
  # By default, rules do not have to find attackers as they do not have
@@ -231,15 +239,17 @@ module Contrast
231
239
  #
232
240
  # @param context [Contrast::Agent::RequestContext] the context for
233
241
  # the current request
234
- # @param ia_result [Contrast::Agent::Reporting::InputAnalysis]
242
+ # @param ia_result [Contrast::Agent::Reporting::Settings::InputAnalysisResult]
235
243
  # @param result [Contrast::Agent::Reporting::AttackResult]
236
244
  # @param attack_string [String] Potential attack vector
237
245
  # @return [Contrast::Agent::Reporting::AttackResult]
238
246
  def update_successful_attack_response context, ia_result, result, attack_string = nil
247
+ cef_outcome = :successful_attack
239
248
  case mode
240
249
  when :MONITOR
241
250
  # We are checking the result as the ia_result would not contain the sub-rules.
242
251
  result.response = if SUSPICIOUS_REPORTING_RULES.include?(result&.rule_id)
252
+ cef_outcome = :suspicious_attack
243
253
  Contrast::Agent::Reporting::ResponseType::SUSPICIOUS
244
254
  else
245
255
  Contrast::Agent::Reporting::ResponseType::MONITORED
@@ -250,7 +260,11 @@ module Contrast
250
260
 
251
261
  ia_result.attack_count = ia_result.attack_count + 1 if ia_result
252
262
  log_rule_matched(context, ia_result, result.response, attack_string)
253
-
263
+ cef_logging(result,
264
+ cef_outcome,
265
+ value: ia_result&.value || attack_string,
266
+ input_type: ia_result&.input_type,
267
+ context: context)
254
268
  result
255
269
  end
256
270
 
@@ -265,17 +279,32 @@ module Contrast
265
279
  # multiple inputs being found to violate the protection criteria
266
280
  # @return [Contrast::Agent::Reporting::AttackResult]
267
281
  def update_perimeter_attack_response context, ia_result, result
268
- if mode == :BLOCK_AT_PERIMETER
282
+ cef_outcome = :successful_attack
283
+ case mode
284
+ when :BLOCK_AT_PERIMETER
269
285
  result.response = if blocked_rule?(ia_result)
270
286
  Contrast::Agent::Reporting::ResponseType::BLOCKED
271
287
  else
272
288
  Contrast::Agent::Reporting::ResponseType::BLOCKED_AT_PERIMETER
273
289
  end
274
290
  log_rule_matched(context, ia_result, result.response)
275
- elsif ia_result.nil? || ia_result.attack_count.zero?
291
+ when :BLOCK && rule_name == Contrast::Agent::Protect::Rule::Xss::NAME
292
+ # Handle cases like reflected-xss:
293
+ result.response = Contrast::Agent::Reporting::ResponseType::BLOCKED
294
+ log_rule_matched(context, ia_result, result.response)
295
+ else
296
+ # Handles all other cases including Reflected-xss in MONITOR mode.
297
+ return unless ia_result.nil? || ia_result.attack_count.zero?
298
+
276
299
  result.response = assign_reporter_response_type(ia_result)
300
+ cef_outcome = suspicious_rule?(ia_result) ? :suspicious_attack : :ineffective_attack
277
301
  log_rule_probed(context, ia_result)
278
302
  end
303
+ cef_logging(result,
304
+ cef_outcome,
305
+ value: ia_result&.value,
306
+ input_type: ia_result&.input_type,
307
+ context: context)
279
308
 
280
309
  result
281
310
  end
@@ -334,7 +363,7 @@ module Contrast
334
363
  # the rule id.
335
364
  #
336
365
  # @param context [Contrast::Agent::RequestContext]
337
- # @return [Array<Contrast::Agent::Reporting::InputAnalysis>]
366
+ # @return [Array<Contrast::Agent::Reporting::InputAnalysisResult>]
338
367
  def gather_ia_results context
339
368
  return [] unless context&.agent_input_analysis&.results
340
369
 
@@ -349,7 +378,8 @@ module Contrast
349
378
  def blocked_violation? result
350
379
  return false unless result
351
380
 
352
- result.response == Contrast::Agent::Reporting::ResponseType::BLOCKED
381
+ blocked? && (result.response == Contrast::Agent::Reporting::ResponseType::BLOCKED ||
382
+ result.response == Contrast::Agent::Reporting::ResponseType::BLOCKED_AT_PERIMETER)
353
383
  end
354
384
 
355
385
  private
@@ -359,7 +389,8 @@ module Contrast
359
389
  def blocked_rule? ia_result
360
390
  [
361
391
  Contrast::Agent::Protect::Rule::Sqli::NAME,
362
- Contrast::Agent::Protect::Rule::NoSqli::NAME
392
+ Contrast::Agent::Protect::Rule::NoSqli::NAME,
393
+ Contrast::Agent::Protect::Rule::Xss::NAME
363
394
  ].include?(ia_result&.rule_id)
364
395
  end
365
396
 
@@ -392,7 +423,7 @@ module Contrast
392
423
 
393
424
  # @param context [Contrast::Agent::RequestContext]
394
425
  # @param potential_attack_string [String, nil]
395
- # @return [Contrast::Agent::Reporting, nil]
426
+ # @return [Contrast::Agent::Reporting::AttackResult, nil]
396
427
  def find_postfilter_attacker context, potential_attack_string, **kwargs
397
428
  ia_results = gather_ia_results(context)
398
429
  ia_results.select! do |ia_result|
@@ -19,6 +19,7 @@ module Contrast
19
19
 
20
20
  NAME = 'bot-blocker'
21
21
  APPLICABLE_USER_INPUTS = [HEADER].cs__freeze
22
+ BLOCK_MESSAGE = 'Bot Blocker rule triggered. Unsafe Bot blocked.'
22
23
 
23
24
  def rule_name
24
25
  NAME
@@ -28,6 +29,13 @@ module Contrast
28
29
  APPLICABLE_USER_INPUTS
29
30
  end
30
31
 
32
+ # Return the specific blocking message for this rule.
33
+ #
34
+ # @return [String] the reason for the raised security exception.
35
+ def block_message
36
+ BLOCK_MESSAGE
37
+ end
38
+
31
39
  # Bot blocker input classification
32
40
  #
33
41
  # @return [module<Contrast::Agent::Protect::Rule::BotBlockerInputClassification>]
@@ -49,13 +57,13 @@ module Contrast
49
57
  ia_result.score_level == Contrast::Agent::Reporting::ScoreLevel::DEFINITEATTACK
50
58
 
51
59
  result = build_attack_without_match(context, ia_result, nil)
52
- append_to_activity(context, result) if result
53
- cef_logging(result, :successful_attack) if result
54
- return unless blocked?
60
+ return unless result
55
61
 
62
+ append_to_activity(context, result)
63
+ record_triggered(context)
56
64
  # Raise BotBlocker error
57
65
  exception_message = "#{ rule_name } rule triggered. Unsafe Bot blocked."
58
- raise(Contrast::SecurityException.new(self, exception_message))
66
+ raise(Contrast::SecurityException.new(self, exception_message)) if blocked_violation?(result)
59
67
  end
60
68
 
61
69
  # @param context [Contrast::Agent::RequestContext]
@@ -22,32 +22,6 @@ module Contrast
22
22
  class << self
23
23
  include Contrast::Agent::Protect::Rule::InputClassification::Base
24
24
 
25
- # Input Classification stage is done to determine if an user input is
26
- # DEFINITEATTACK or to be ignored.
27
- #
28
- # @param rule_id [String] Name of the protect rule.
29
- # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
30
- # @param value [Hash<String>] the value of the input.
31
- # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
32
- # agent analysis from the current
33
- # Request.
34
- # @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
35
- def classify rule_id, input_type, value, input_analysis
36
- return unless (rule = Contrast::PROTECT.rule(rule_id))
37
- return unless rule.applicable_user_inputs.include?(input_type)
38
- return unless input_analysis.request
39
-
40
- value.each_value do |val|
41
- result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, val)
42
- append_result(input_analysis, result)
43
- end
44
-
45
- input_analysis
46
- rescue StandardError => e
47
- logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
48
- nil
49
- end
50
-
51
25
  private
52
26
 
53
27
  # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
@@ -84,12 +84,9 @@ module Contrast
84
84
  return unless result
85
85
 
86
86
  append_to_activity(context, result)
87
- cef_logging(result, :successful_attack)
88
-
89
- return unless blocked?
90
-
87
+ record_triggered(context)
91
88
  # Raise cmdi error
92
- raise_error(classname, method)
89
+ raise_error(classname, method) if blocked_violation?(result)
93
90
  end
94
91
  end
95
92
  end
@@ -43,10 +43,8 @@ module Contrast
43
43
  **{ classname: classname, method: method }))
44
44
 
45
45
  append_to_activity(context, result)
46
- cef_logging(result, :successful_attack)
47
- return unless blocked?
48
-
49
- raise_error(classname, method)
46
+ record_triggered(context)
47
+ raise_error(classname, method) if blocked_violation?(result)
50
48
  end
51
49
 
52
50
  private
@@ -20,7 +20,7 @@ module Contrast
20
20
  APPLICABLE_USER_INPUTS = [
21
21
  BODY, COOKIE_VALUE, HEADER, PARAMETER_NAME,
22
22
  PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE,
23
- MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
23
+ MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE, UNKNOWN
24
24
  ].cs__freeze
25
25
 
26
26
  # CMDI input classification
@@ -46,6 +46,7 @@ module Contrast
46
46
  return unless (result = build_violation(context, command))
47
47
 
48
48
  append_to_activity(context, result)
49
+ record_triggered(context)
49
50
  raise_error(classname, method) if blocked_violation?(result)
50
51
  end
51
52
 
@@ -50,6 +50,7 @@ module Contrast
50
50
  end
51
51
 
52
52
  # Return the specific blocking message for this rule.
53
+ #
53
54
  # @return [String] the reason for the raised security exception.
54
55
  def block_message
55
56
  BLOCK_MESSAGE
@@ -84,10 +85,9 @@ module Contrast
84
85
  kwargs = { GADGET_TYPE: gadget }
85
86
  result = build_attack_with_match(context, ia_result, nil, serialized_input, **kwargs)
86
87
  append_to_activity(context, result)
88
+ record_triggered(context)
87
89
 
88
- cef_logging(result, :successful_attack)
89
-
90
- raise(Contrast::SecurityException.new(self, block_message)) if blocked?
90
+ raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
91
91
  end
92
92
 
93
93
  # Determine if the issued command was called while we're
@@ -106,13 +106,13 @@ module Contrast
106
106
  ia_result = build_evaluation(gadget_command)
107
107
  result = build_attack_with_match(context, ia_result, nil, gadget_command, **kwargs)
108
108
  append_to_activity(context, result)
109
- cef_logging(result, :successful_attack, value: gadget_command)
110
109
  raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked?
111
110
  end
112
111
 
113
112
  protected
114
113
 
115
114
  # Build the RaspRuleSample for the detected Deserialization attack.
115
+ #
116
116
  # @param context [Contrast::Agent::RequestContext] the request
117
117
  # context in which this attack is occurring.
118
118
  # @param input_analysis_result [Contrast::Agent::Reporting::InputAnalysis]
@@ -24,7 +24,7 @@ module Contrast
24
24
  COOKIE_VALUE, PARAMETER_VALUE, HEADER, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE
25
25
  ].cs__freeze
26
26
 
27
- BASE64_INPUT_TYPES = [BODY, COOKIE_VALUE, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
27
+ BASE64_INPUT_TYPES = [BODY, HEADER, COOKIE_VALUE, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
28
28
 
29
29
  class << self
30
30
  include Contrast::Components::Logger::InstanceMethods
@@ -183,9 +183,6 @@ module Contrast
183
183
  return value unless Contrast::PROTECT.normalize_base64?
184
184
  return value unless BASE64_INPUT_TYPES.include?(input_type)
185
185
 
186
- # TODO: RUBY-2110 Update the HEADER handling if possible.
187
- # We need only the Header values.
188
-
189
186
  cs__decode64(value, input_type)
190
187
  end
191
188
  end