contrast-agent 7.4.0 → 7.5.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 (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