contrast-agent 7.2.0 → 7.3.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/protect/input_analyzer/input_analyzer.rb +62 -23
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
- data/lib/contrast/agent/protect/rule/base.rb +5 -1
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
- data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
- data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
- data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
- data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
- data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
- data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
- data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
- data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
- data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
- data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
- data/lib/contrast/agent/telemetry/base.rb +28 -2
- data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/client.rb +10 -2
- data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
- data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
- data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
- data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
- data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
- data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/config.rb +4 -4
- data/lib/contrast/components/protect.rb +11 -1
- data/lib/contrast/components/sampling.rb +15 -10
- data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
- data/lib/contrast/config/yaml_file.rb +8 -0
- data/lib/contrast/framework/rails/support.rb +3 -0
- data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
- data/lib/contrast/utils/metrics_hash.rb +1 -1
- data/lib/contrast/utils/object_share.rb +2 -1
- data/lib/contrast/utils/response_utils.rb +12 -0
- data/lib/contrast/utils/timer.rb +2 -0
- data/lib/contrast.rb +9 -2
- data/ruby-agent.gemspec +1 -1
- metadata +21 -6
- data/lib/contrast/utils/input_classification_base.rb +0 -169
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e64852411fae5dd5c52973361e7f54cdf60813105423ed81cd6455c6ec212330
|
4
|
+
data.tar.gz: d7d6a1ef01242d97b36f1b8fc4767829d300ec2f3ce1dcb21df7fe05aebc22b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86626808971cfc1fcd74febe8cdf2d41be668a78cc02d41d4bf7f6a488e550fef9f2a8fe3230eea7ea9ed63824d09e61956878d34d99375dd3595b77d3a9755f
|
7
|
+
data.tar.gz: 9fb15e1733095b1f7c2a9d5d4bfce96a3a2e9d5f956d77f0cd6c1fb810c44166a7bf0e2b89966a63ca80aa34514dd82e6f87e619a782d5024854d84c1ed93183
|
@@ -14,10 +14,13 @@ require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input
|
|
14
14
|
require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload'
|
15
15
|
require 'contrast/agent/protect/rule/path_traversal/path_traversal'
|
16
16
|
require 'contrast/agent/protect/rule/path_traversal/path_traversal_input_classification'
|
17
|
+
require 'contrast/agent/protect/rule/input_classification/lru_cache'
|
18
|
+
require 'contrast/agent/protect/rule/input_classification/cached_result'
|
17
19
|
require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
|
18
20
|
require 'contrast/agent/protect/rule/xss/xss'
|
19
21
|
require 'contrast/components/logger'
|
20
22
|
require 'contrast/utils/object_share'
|
23
|
+
require 'contrast/agent/protect/rule/input_classification/base64_statistic'
|
21
24
|
require 'json'
|
22
25
|
|
23
26
|
module Contrast
|
@@ -35,6 +38,8 @@ module Contrast
|
|
35
38
|
].cs__freeze
|
36
39
|
POSTFILTER_RULES = %w[sql-injection cmd-injection reflected-xss path-traversal nosql-injection].cs__freeze
|
37
40
|
AGENTLIB_TIMEOUT = 5.cs__freeze
|
41
|
+
TIMEOUT_ERROR_MESSAGE = '[AgentLib] Timed out when processing InputAnalysisResult'
|
42
|
+
STANDARD_ERROR_MESSAGE = '[InputAnalyzer] Exception raise while doing input analysis:'
|
38
43
|
|
39
44
|
class << self
|
40
45
|
include Contrast::Agent::Reporting::InputType
|
@@ -42,6 +47,18 @@ module Contrast
|
|
42
47
|
include Contrast::Utils::ObjectShare
|
43
48
|
include Contrast::Components::Logger::InstanceMethods
|
44
49
|
|
50
|
+
# Cache for storing the input analysis result per rule
|
51
|
+
#
|
52
|
+
# @return [Contrast::Agent::Protect::Rule::InputClassification::LRUCache]
|
53
|
+
def lru_cache
|
54
|
+
@_lru_cache ||= Contrast::Agent::Protect::Rule::InputClassification::LRUCache.new
|
55
|
+
end
|
56
|
+
|
57
|
+
# Input decoding statistic.
|
58
|
+
def base64_statistic
|
59
|
+
@_base64_statistic ||= Contrast::Agent::Protect::Rule::InputClassification::Base64Statistic.new
|
60
|
+
end
|
61
|
+
|
45
62
|
# This method with analyze the user input from the context of the
|
46
63
|
# current request and return new ia with extracted input types.
|
47
64
|
#
|
@@ -51,13 +68,13 @@ module Contrast
|
|
51
68
|
return unless Contrast::PROTECT.enabled?
|
52
69
|
return if request.nil?
|
53
70
|
|
54
|
-
inputs =
|
71
|
+
inputs = extract_inputs(request)
|
55
72
|
return unless inputs
|
56
73
|
|
57
74
|
input_analysis = Contrast::Agent::Reporting::InputAnalysis.new
|
58
75
|
input_analysis.request = request
|
59
76
|
# Save those for trigger time
|
60
|
-
input_analysis.inputs =
|
77
|
+
input_analysis.inputs = inputs
|
61
78
|
input_analysis
|
62
79
|
end
|
63
80
|
|
@@ -69,16 +86,9 @@ module Contrast
|
|
69
86
|
#
|
70
87
|
# @param request [Contrast::Agent::Request] current request context.
|
71
88
|
# @return inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
|
72
|
-
def
|
89
|
+
def extract_inputs request
|
73
90
|
inputs = {}
|
74
|
-
inputs
|
75
|
-
inputs[COOKIE_NAME] = request.cookies.keys
|
76
|
-
inputs[COOKIE_VALUE] = request.cookies.values
|
77
|
-
inputs[HEADER] = request.headers
|
78
|
-
inputs[PARAMETER_NAME] = request.parameters.keys
|
79
|
-
inputs[PARAMETER_VALUE] = request.parameters.values
|
80
|
-
inputs[QUERYSTRING] = request.query_string
|
81
|
-
inputs[METHOD] = request.request_method
|
91
|
+
extract_request_inputs(inputs, request)
|
82
92
|
extract_multipart(inputs, request)
|
83
93
|
inputs.compact!
|
84
94
|
inputs
|
@@ -86,22 +96,29 @@ module Contrast
|
|
86
96
|
|
87
97
|
# classify input by rule
|
88
98
|
#
|
89
|
-
# @param rule_id [String] name of the rule
|
90
|
-
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from
|
91
|
-
#
|
92
|
-
def input_classification_for rule_id, input_analysis
|
99
|
+
# @param rule_id [String] name of the rule.
|
100
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from analyze method.
|
101
|
+
# @param interval [Integer] The timeout determined for the AgentLib analysis to be performed.
|
102
|
+
def input_classification_for rule_id, input_analysis, interval: AGENTLIB_TIMEOUT
|
93
103
|
return unless input_analysis&.inputs
|
94
104
|
return unless (protect_rule = Contrast::PROTECT.rule(rule_id)) && protect_rule.enabled?
|
95
105
|
|
96
106
|
input_analysis.inputs.each do |input_type, value|
|
97
107
|
next if value.nil? || value.empty?
|
98
108
|
|
99
|
-
|
109
|
+
Timeout.timeout(interval) do
|
110
|
+
protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
|
111
|
+
end
|
100
112
|
end
|
101
113
|
|
102
114
|
input_analysis
|
103
115
|
rescue StandardError => e
|
104
|
-
|
116
|
+
if e.cs__class == Timeout::Error
|
117
|
+
log_error(rule_id, TIMEOUT_ERROR_MESSAGE, e)
|
118
|
+
else
|
119
|
+
log_error(rule_id, STANDARD_ERROR_MESSAGE, e, level: :error)
|
120
|
+
end
|
121
|
+
nil
|
105
122
|
end
|
106
123
|
|
107
124
|
# classify input by array of rules. There is a timeout for the AgentLib analysis if not set it
|
@@ -134,14 +151,9 @@ module Contrast
|
|
134
151
|
# Check to see if rules is already triggered only for infilter:
|
135
152
|
next if input_analysis.triggered_rules.include?(rule_id) && infilter
|
136
153
|
|
137
|
-
|
138
|
-
input_classification_for(rule_id, input_analysis)
|
139
|
-
end
|
154
|
+
input_classification_for(rule_id, input_analysis, interval: interval)
|
140
155
|
end
|
141
156
|
input_analysis
|
142
|
-
rescue Timeout::Error => e
|
143
|
-
logger.warn('AgentLib timed out when processing InputAnalysisResult', e, ia_result)
|
144
|
-
nil
|
145
157
|
end
|
146
158
|
|
147
159
|
private
|
@@ -158,6 +170,33 @@ module Contrast
|
|
158
170
|
name = filename[DISPOSITION_NAME.to_sym]
|
159
171
|
inputs[MULTIPART_NAME] = name if name
|
160
172
|
end
|
173
|
+
|
174
|
+
# Extract the parameters and query string from the request context.
|
175
|
+
#
|
176
|
+
# @param inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
|
177
|
+
# @param request [Contrast::Agent::Request] current request context.
|
178
|
+
def extract_request_inputs inputs, request
|
179
|
+
inputs[BODY] = request.body
|
180
|
+
inputs[COOKIE_NAME] = request.cookies.keys
|
181
|
+
inputs[COOKIE_VALUE] = request.cookies.values
|
182
|
+
inputs[HEADER] = request.headers
|
183
|
+
inputs[METHOD] = request.request_method
|
184
|
+
inputs[PARAMETER_NAME] = request.parameters.keys
|
185
|
+
inputs[PARAMETER_VALUE] = request.parameters.values
|
186
|
+
inputs[QUERYSTRING] = request.query_string
|
187
|
+
end
|
188
|
+
|
189
|
+
# Logs any errrors that occur during the analysis
|
190
|
+
# Accepts a level parameter to determine if the error should be logged as an error or warning.
|
191
|
+
#
|
192
|
+
# @param rule_id [String] name of the rule.
|
193
|
+
def log_error rule_id, message, error, level: :error
|
194
|
+
if level == :error
|
195
|
+
logger.error(message, rule_id: rule_id, error: error)
|
196
|
+
else
|
197
|
+
logger.warn(message, rule_id: rule_id, error: error)
|
198
|
+
end
|
199
|
+
end
|
161
200
|
end
|
162
201
|
end
|
163
202
|
end
|
@@ -5,7 +5,10 @@ require 'contrast/agent/thread/worker_thread'
|
|
5
5
|
require 'contrast/agent/reporting/input_analysis/input_analysis_result'
|
6
6
|
require 'contrast/agent/reporting/input_analysis/score_level'
|
7
7
|
require 'contrast/agent/reporting/reporting_events/application_activity'
|
8
|
-
require 'contrast/
|
8
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
9
|
+
require 'contrast/agent/telemetry/input_analysis_cache_event'
|
10
|
+
require 'contrast/agent/telemetry/input_analysis_encoding_event'
|
11
|
+
require 'contrast/utils/reporting/application_activity_batch_utils'
|
9
12
|
|
10
13
|
module Contrast
|
11
14
|
module Agent
|
@@ -15,7 +18,8 @@ module Contrast
|
|
15
18
|
# Currently only includes: cmd_injection & sqli_injection rules
|
16
19
|
class WorthWatchingInputAnalyzer < WorkerThread
|
17
20
|
include Timeout
|
18
|
-
include Contrast::Agent::Protect::Rule::
|
21
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
22
|
+
include Contrast::Utils::Reporting::ApplicationActivityBatchUtils
|
19
23
|
|
20
24
|
QUEUE_SIZE = 1000.cs__freeze
|
21
25
|
AGENTLIB_TIMEOUT = 5.cs__freeze
|
@@ -48,8 +52,10 @@ module Contrast
|
|
48
52
|
activity.attach_defend(attack_result)
|
49
53
|
report = true
|
50
54
|
end
|
51
|
-
|
52
|
-
|
55
|
+
report_activity(activity) if report
|
56
|
+
# Handle reporting of IA Cache statistics:
|
57
|
+
enqueue_cache_event(stored_ia.request)
|
58
|
+
enqueue_encoding_event(stored_ia.request)
|
53
59
|
rescue StandardError => e
|
54
60
|
logger.error('[WorthWatchingAnalyzer] thread could not process result because of:', e)
|
55
61
|
end
|
@@ -73,6 +79,27 @@ module Contrast
|
|
73
79
|
|
74
80
|
private
|
75
81
|
|
82
|
+
# After we have finished with all IA results, we need to send the cache statistics to Telemetry.
|
83
|
+
# Now the request cycle is finished and we can send the cache statistics.
|
84
|
+
#
|
85
|
+
# @param request [Contrast::Agent::Request] stored request.
|
86
|
+
def enqueue_cache_event request
|
87
|
+
return unless Contrast::Agent::Telemetry::Base.enabled?
|
88
|
+
|
89
|
+
Contrast::TELEMETRY_IA_CACHE[request.__id__] = Contrast::Agent::Protect::InputAnalyzer.
|
90
|
+
lru_cache.statistics.to_events.dup
|
91
|
+
Contrast::Agent::Protect::InputAnalyzer.lru_cache.clear_statistics
|
92
|
+
end
|
93
|
+
|
94
|
+
def enqueue_encoding_event request
|
95
|
+
return unless Contrast::Agent::Telemetry::Base.enabled?
|
96
|
+
return unless Contrast::PROTECT.normalize_base64?
|
97
|
+
|
98
|
+
Contrast::TELEMETRY_BASE64_HASH[request.__id__] = Contrast::Agent::Protect::InputAnalyzer.
|
99
|
+
base64_statistic.to_events.dup
|
100
|
+
Contrast::Agent::Protect::InputAnalyzer.base64_statistic.clear
|
101
|
+
end
|
102
|
+
|
76
103
|
# This method will build the attack results from the saved ia.
|
77
104
|
#
|
78
105
|
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
|
@@ -114,6 +141,12 @@ module Contrast
|
|
114
141
|
@_queue ||= Queue.new
|
115
142
|
end
|
116
143
|
|
144
|
+
def report_activity activity
|
145
|
+
logger.debug('[WorthWatchingAnalyzer] preparing to send activity batch')
|
146
|
+
add_activity_to_batch(activity)
|
147
|
+
report_batch
|
148
|
+
end
|
149
|
+
|
117
150
|
def delete_queue!
|
118
151
|
@_queue&.clear
|
119
152
|
@_queue&.close
|
@@ -63,6 +63,10 @@ module Contrast
|
|
63
63
|
RULE_NAME
|
64
64
|
end
|
65
65
|
|
66
|
+
# Should return the short name.
|
67
|
+
#
|
68
|
+
# @return [String]
|
69
|
+
|
66
70
|
# Should return list of all sub_rules.
|
67
71
|
# Extend for each main rule any sub-rules.
|
68
72
|
#
|
@@ -328,7 +332,7 @@ module Contrast
|
|
328
332
|
# @param context [Contrast::Agent::RequestContext]
|
329
333
|
# @return [Array<Contrast::Agent::Reporting::InputAnalysis>]
|
330
334
|
def gather_ia_results context
|
331
|
-
return
|
335
|
+
return [] unless context&.agent_input_analysis&.results
|
332
336
|
|
333
337
|
context.agent_input_analysis.results.select do |ia_result|
|
334
338
|
ia_result.rule_id == rule_name && ia_result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE
|
@@ -4,7 +4,7 @@
|
|
4
4
|
require 'contrast/agent/reporting/input_analysis/input_type'
|
5
5
|
require 'contrast/agent/reporting/input_analysis/score_level'
|
6
6
|
require 'contrast/agent/reporting/details/bot_blocker_details'
|
7
|
-
require 'contrast/
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
8
8
|
require 'contrast/utils/object_share'
|
9
9
|
|
10
10
|
module Contrast
|
@@ -20,7 +20,7 @@ module Contrast
|
|
20
20
|
BOT_BLOCKER_MATCH = 'bot-blocker-input-tracing-v1'
|
21
21
|
|
22
22
|
class << self
|
23
|
-
include
|
23
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
24
24
|
|
25
25
|
# Input Classification stage is done to determine if an user input is
|
26
26
|
# DEFINITEATTACK or to be ignored.
|
@@ -45,6 +45,7 @@ module Contrast
|
|
45
45
|
input_analysis
|
46
46
|
rescue StandardError => e
|
47
47
|
logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
|
48
|
+
nil
|
48
49
|
end
|
49
50
|
|
50
51
|
private
|
@@ -57,19 +58,35 @@ module Contrast
|
|
57
58
|
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
58
59
|
# @param value [String, Array<String>] the value of the input.
|
59
60
|
#
|
60
|
-
# @return res [Contrast::Agent::Reporting::InputAnalysisResult]
|
61
|
+
# @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil]
|
61
62
|
def create_new_input_result request, rule_id, input_type, value
|
62
63
|
return unless request.headers.key(value) == USER_AGENT
|
63
|
-
return unless Contrast::AGENT_LIB
|
64
64
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
super(request, rule_id, input_type, value)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Creates new instance of AgentLib evaluation result with direct call to AgentLib.
|
69
|
+
#
|
70
|
+
# @param rule_id [String] The name of the Protect Rule.
|
71
|
+
# @param _input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
72
|
+
# @param value [String, Array<String>] the value of the input.
|
73
|
+
def build_input_eval rule_id, _input_type, value
|
74
|
+
Contrast::AGENT_LIB.eval_header(AGENT_LIB_HEADER_NAME,
|
75
|
+
value,
|
76
|
+
Contrast::AGENT_LIB.rule_set[rule_id],
|
77
|
+
Contrast::AGENT_LIB.eval_option[:NONE])
|
78
|
+
end
|
70
79
|
|
80
|
+
# Creates specific result from the AgentLib evaluation.
|
81
|
+
#
|
82
|
+
# @param rule_id [String] The name of the Protect Rule.
|
83
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
84
|
+
# @param value [String, Array<String>] the value of the input.
|
85
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
86
|
+
# @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
|
87
|
+
def build_ia_result rule_id, input_type, value, request, input_eval
|
71
88
|
ia_result = new_ia_result(rule_id, input_type, request.path, value)
|
72
|
-
score =
|
89
|
+
score = input_eval&.score || 0
|
73
90
|
if score >= THRESHOLD
|
74
91
|
ia_result.score_level = DEFINITEATTACK
|
75
92
|
ia_result.ids << BOT_BLOCKER_MATCH
|
@@ -79,7 +96,6 @@ module Contrast
|
|
79
96
|
else
|
80
97
|
ia_result.score_level = IGNORE
|
81
98
|
end
|
82
|
-
add_needed_key(request, ia_result, input_type, value)
|
83
99
|
ia_result
|
84
100
|
end
|
85
101
|
|
@@ -43,7 +43,6 @@ module Contrast
|
|
43
43
|
# to BLOCK and valid cdmi is detected.
|
44
44
|
def infilter context, classname, method, command
|
45
45
|
return unless infilter?(command)
|
46
|
-
return if protect_excluded_by_url?(rule_name)
|
47
46
|
return unless (result = build_violation(context, command))
|
48
47
|
|
49
48
|
append_to_activity(context, result)
|
@@ -4,7 +4,7 @@
|
|
4
4
|
require 'contrast/agent/protect/rule/cmdi/cmd_injection'
|
5
5
|
require 'contrast/agent/reporting/input_analysis/score_level'
|
6
6
|
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
7
|
-
require 'contrast/
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
8
8
|
require 'contrast/components/logger'
|
9
9
|
|
10
10
|
module Contrast
|
@@ -17,7 +17,7 @@ module Contrast
|
|
17
17
|
module CmdiInputClassification
|
18
18
|
WORTHWATCHING_MATCH = 'cmdi-worth-watching-v2'.cs__freeze
|
19
19
|
class << self
|
20
|
-
include
|
20
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
21
21
|
include Contrast::Components::Logger::InstanceMethods
|
22
22
|
end
|
23
23
|
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/utils/object_share'
|
5
|
+
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
6
|
+
require 'contrast/agent/protect/rule/input_classification/extendable'
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/encoding'
|
8
|
+
require 'contrast/components/logger'
|
9
|
+
|
10
|
+
module Contrast
|
11
|
+
module Agent
|
12
|
+
module Protect
|
13
|
+
module Rule
|
14
|
+
module InputClassification
|
15
|
+
# This module will include all the similar information for all input classifications
|
16
|
+
# between different rules
|
17
|
+
module Base
|
18
|
+
UNKNOWN_KEY = 'unknown'
|
19
|
+
include Contrast::Components::Logger::InstanceMethods
|
20
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Extendable
|
21
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Encoding
|
22
|
+
|
23
|
+
KEYS_NEEDED = [
|
24
|
+
COOKIE_VALUE, PARAMETER_VALUE, HEADER, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE
|
25
|
+
].cs__freeze
|
26
|
+
|
27
|
+
BASE64_INPUT_TYPES = [BODY, COOKIE_VALUE, HEADER, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
|
28
|
+
|
29
|
+
class << self
|
30
|
+
include Contrast::Components::Logger::InstanceMethods
|
31
|
+
include Contrast::Agent::Reporting::InputType
|
32
|
+
|
33
|
+
# Finds key value and type based on input type and value.
|
34
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
35
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
36
|
+
# @param value [String, Array<String>] the value of the input.
|
37
|
+
# @return [Array<(String, Contrast::Agent::Reporting::InputType)>] key and key type.
|
38
|
+
def find_key request, input_type, value
|
39
|
+
# TODO: RUBY-99999 Add handling for multipart, json and if any missing types.
|
40
|
+
case input_type
|
41
|
+
when COOKIE_VALUE
|
42
|
+
[request.cookies.key(value), Contrast::Agent::Reporting::InputType::COOKIE_NAME]
|
43
|
+
when PARAMETER_VALUE, URL_PARAMETER
|
44
|
+
[request.parameters.key(value), Contrast::Agent::Reporting::InputType::PARAMETER_NAME]
|
45
|
+
when HEADER
|
46
|
+
[request.headers.key(value), Contrast::Agent::Reporting::InputType::HEADER]
|
47
|
+
when UNKNOWN
|
48
|
+
[UNKNOWN_KEY, Contrast::Agent::Reporting::InputType::UNKNOWN]
|
49
|
+
else
|
50
|
+
[nil, nil]
|
51
|
+
end
|
52
|
+
rescue StandardError => e
|
53
|
+
logger.warn('[InputAnalyzer] Could not find proper key for input traced value', message: e)
|
54
|
+
[nil, nil]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Some input types are not yet supported from the AgentLib.
|
58
|
+
# This will convert the type to the closet possible if viable,
|
59
|
+
# so that the input tracing could be done.
|
60
|
+
#
|
61
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
62
|
+
# @return [Integer<Contrast::AgentLib::Interface::INPUT_SET>]
|
63
|
+
def convert_input_type input_type
|
64
|
+
case input_type
|
65
|
+
when URI, URL_PARAMETER
|
66
|
+
Contrast::AGENT_LIB.input_set[:URI_PATH]
|
67
|
+
when BODY, DWR_VALUE, SOCKET, UNDEFINED_TYPE, UNKNOWN, REQUEST, QUERYSTRING
|
68
|
+
Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
|
69
|
+
when HEADER
|
70
|
+
Contrast::AGENT_LIB.input_set[:HEADER_VALUE]
|
71
|
+
when MULTIPART_VALUE, MULTIPART_FIELD_NAME
|
72
|
+
Contrast::AGENT_LIB.input_set[:MULTIPART_NAME]
|
73
|
+
when JSON_ARRAYED_VALUE
|
74
|
+
Contrast::AGENT_LIB.input_set[:JSON_KEY]
|
75
|
+
when PARAMETER_NAME
|
76
|
+
Contrast::AGENT_LIB.input_set[:PARAMETER_KEY]
|
77
|
+
else
|
78
|
+
Contrast::AGENT_LIB.input_set[input_type]
|
79
|
+
end
|
80
|
+
rescue StandardError => e
|
81
|
+
logger.debug('[InputAnalyzer] Protect Input classification could not determine input type,
|
82
|
+
falling back to default',
|
83
|
+
error: e)
|
84
|
+
Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Input Classification stage is done to determine if an user input is
|
89
|
+
# DEFINITEATTACK or to be ignored.
|
90
|
+
#
|
91
|
+
# @param rule_id [String] Name of the protect rule.
|
92
|
+
# @param input_type [Symbol, Contrast::Agent::Reporting::InputType] The type of the user input.
|
93
|
+
# @param value [String, Array<String>] the value of the input.
|
94
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
|
95
|
+
# agent analysis from the current
|
96
|
+
# Request.
|
97
|
+
# @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
|
98
|
+
def classify rule_id, input_type, value, input_analysis
|
99
|
+
return unless (rule = Contrast::PROTECT.rule(rule_id))
|
100
|
+
return unless rule.applicable_user_inputs.include?(input_type)
|
101
|
+
return unless input_analysis.request
|
102
|
+
|
103
|
+
Array(value).each do |val|
|
104
|
+
Array(val).each do |v|
|
105
|
+
next unless v
|
106
|
+
|
107
|
+
result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, v)
|
108
|
+
append_result(input_analysis, result)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
input_analysis
|
113
|
+
rescue StandardError => e
|
114
|
+
logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
# This methods checks if input is value that matches a key in the input.
|
119
|
+
#
|
120
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
121
|
+
# @param ia_result [Contrast::Agent::Reporting::InputAnalysisResult] result to be updated.
|
122
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
123
|
+
# @param value [String, Array<String>] the value of the input.
|
124
|
+
#
|
125
|
+
# @return result [Array<String, Symbol>] updated with key result.
|
126
|
+
def add_needed_key request, ia_result, input_type, value
|
127
|
+
ia_result.key, ia_result.key_type = Contrast::Agent::Protect::Rule::InputClassification::Base.
|
128
|
+
find_key(request, input_type, value)
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Appends result to the InputAnalysis.
|
134
|
+
#
|
135
|
+
# @param ia_analysis [Contrast::Agent::Reporting::InputAnalysis] the current input analysis.
|
136
|
+
# @param result [Contrast::Agent::Reporting::InputAnalysisResult] result to be appended.
|
137
|
+
# @return [Contrast::Agent::Reporting::InputAnalysis] the input analysis with the appended result.
|
138
|
+
def append_result ia_analysis, result
|
139
|
+
ia_analysis.results << result if result
|
140
|
+
ia_analysis
|
141
|
+
end
|
142
|
+
|
143
|
+
# Do not override this method, it will hold base operations, instead overwrite methods called inside
|
144
|
+
# of this method.
|
145
|
+
# This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
|
146
|
+
# key if needed and Creates new instance of InputAnalysisResult.
|
147
|
+
#
|
148
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
149
|
+
# @param rule_id [String] The name of the Protect Rule.
|
150
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
151
|
+
# @param value [String, Array<String>] the value of the input.
|
152
|
+
#
|
153
|
+
# @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil]
|
154
|
+
def create_new_input_result request, rule_id, input_type, value
|
155
|
+
return unless Contrast::AGENT_LIB
|
156
|
+
|
157
|
+
# Cache retrieve
|
158
|
+
cached = Contrast::Agent::Protect::InputAnalyzer.lru_cache.lookout(rule_id, value, input_type, request)
|
159
|
+
return cached.result if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
|
160
|
+
|
161
|
+
# Input evaluation
|
162
|
+
input_eval = build_input_eval(rule_id, input_type, base64_decode_input(value, input_type))
|
163
|
+
ia_result = build_ia_result(rule_id, input_type, value, request, input_eval)
|
164
|
+
return unless ia_result
|
165
|
+
|
166
|
+
add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
|
167
|
+
# Input evaluation end
|
168
|
+
|
169
|
+
# Cache save. Cache must be saved after the input evaluation is completed.
|
170
|
+
Contrast::Agent::Protect::InputAnalyzer.lru_cache.save(rule_id, ia_result, request)
|
171
|
+
ia_result
|
172
|
+
end
|
173
|
+
|
174
|
+
# Decodes the value for the given input type.
|
175
|
+
# Applies to BODY, COOKIE_VALUE, HEADER, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE
|
176
|
+
#
|
177
|
+
# @param value [String]
|
178
|
+
# @param input_type [Symbol]
|
179
|
+
# @return input [String]
|
180
|
+
def base64_decode_input value, input_type
|
181
|
+
return value unless Contrast::PROTECT.normalize_base64?
|
182
|
+
return value unless BASE64_INPUT_TYPES.include?(input_type)
|
183
|
+
|
184
|
+
cs__decode64(value, input_type)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/agent/protect/rule/input_classification/encoding_rates'
|
5
|
+
require 'contrast/agent/telemetry/input_analysis_encoding_event'
|
6
|
+
require 'contrast/components/logger'
|
7
|
+
|
8
|
+
module Contrast
|
9
|
+
module Agent
|
10
|
+
module Protect
|
11
|
+
module Rule
|
12
|
+
module InputClassification
|
13
|
+
# This class will safe all the information for the Base64 decoding matches per input type.
|
14
|
+
class Base64Statistic
|
15
|
+
include Contrast::Components::Logger::InstanceMethods
|
16
|
+
|
17
|
+
# @return [Hash<Contrast::Agent::Protect::Rule::InputClassification::EncodingRates>]
|
18
|
+
attr_reader :data
|
19
|
+
|
20
|
+
# Capacity for request context life cycle.
|
21
|
+
CAPACITY = 1000
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@data = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add a match for the given input type.
|
28
|
+
#
|
29
|
+
# @param input_type [Symbol]
|
30
|
+
def match! input_type
|
31
|
+
return @data[input_type]&.increase_match_base64 if @data[input_type]
|
32
|
+
|
33
|
+
@data[input_type] = Contrast::Agent::Protect::Rule::InputClassification::EncodingRates.new(input_type)
|
34
|
+
@data[input_type].increase_match_base64
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add a mismatch for the given input type.
|
38
|
+
#
|
39
|
+
# @param input_type [Symbol]
|
40
|
+
def mismatch! input_type
|
41
|
+
return @data[input_type]&.increase_mismatch_base64 if @data[input_type]
|
42
|
+
|
43
|
+
@data[input_type] = Contrast::Agent::Protect::Rule::InputClassification::EncodingRates.new(input_type)
|
44
|
+
@data[input_type].increase_mismatch_base64
|
45
|
+
end
|
46
|
+
|
47
|
+
# Clears statistic data.
|
48
|
+
def clear
|
49
|
+
@data.clear
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Array<Contrast::Agent::Telemetry::InputAnalysisCacheEvent>] the events to be sent.
|
53
|
+
def to_events
|
54
|
+
events = []
|
55
|
+
data.each do |_input_type, encoding_rate|
|
56
|
+
event = Contrast::Agent::Telemetry::InputAnalysisEncodingEvent.new(nil, encoding_rate)
|
57
|
+
next if event.empty?
|
58
|
+
|
59
|
+
events << event
|
60
|
+
end
|
61
|
+
events
|
62
|
+
rescue StandardError => e
|
63
|
+
logger.error("[Telemetry] Error while creating events: #{ e }", stacktrace: e.backtrace)
|
64
|
+
[]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|