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.
- checksums.yaml +4 -4
- data/lib/contrast/agent/hooks/at_exit_hook.rb +16 -1
- data/lib/contrast/agent/middleware/middleware.rb +1 -1
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +19 -12
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +55 -20
- data/lib/contrast/agent/protect/policy/rule_applicator.rb +1 -4
- data/lib/contrast/agent/protect/rule/base.rb +56 -25
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +12 -4
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +0 -26
- data/lib/contrast/agent/protect/rule/cmdi/cmd_injection.rb +2 -5
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +2 -4
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +2 -1
- data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +4 -4
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +1 -4
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +34 -2
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli.rb +5 -2
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal.rb +12 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +2 -2
- data/lib/contrast/agent/protect/rule/sqli/sqli_base_rule.rb +2 -3
- data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +3 -4
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload.rb +3 -0
- data/lib/contrast/agent/protect/rule/utils/builders.rb +3 -4
- data/lib/contrast/agent/protect/rule/utils/filters.rb +32 -16
- data/lib/contrast/agent/protect/rule/xss/xss.rb +80 -0
- data/lib/contrast/agent/protect/rule/xxe/xxe.rb +9 -2
- data/lib/contrast/agent/reporting/details/xss_match.rb +17 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +32 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +4 -34
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +1 -5
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +2 -5
- data/lib/contrast/agent/reporting/reporting_utilities/build_preflight.rb +4 -4
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +5 -1
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +1 -1
- data/lib/contrast/agent/request/request_context_extend.rb +0 -2
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/assess.rb +4 -0
- data/lib/contrast/framework/rails/support.rb +2 -2
- data/lib/contrast/logger/cef_log.rb +30 -4
- data/lib/contrast/utils/io_util.rb +3 -0
- data/lib/contrast/utils/json.rb +1 -1
- data/lib/contrast/utils/log_utils.rb +21 -10
- data/ruby-agent.gemspec +3 -2
- metadata +18 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6b34ada48cf9e91e05d85ea0f003575119b751bf135ef49709fb1501c475b3e
|
4
|
+
data.tar.gz: 2915fb037c832fec213dfc7835b8d732e1c22005987ef2c4287dfb55264d41a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
46
|
-
|
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
|
-
#
|
66
|
-
|
67
|
-
|
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 <<
|
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
|
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
|
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(
|
114
|
-
results =
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
152
|
-
input_value = sample
|
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
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
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::
|
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
|
-
|
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
|
-
|
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::
|
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
|
-
|
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
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
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
|