contrast-agent 6.9.0 → 6.10.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/.gitignore +1 -1
- data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -1
- data/lib/contrast/agent/middleware.rb +4 -2
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +76 -83
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +40 -35
- data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +2 -0
- data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +6 -3
- data/lib/contrast/agent/protect/policy/applies_path_traversal_rule.rb +3 -0
- data/lib/contrast/agent/protect/policy/applies_sqli_rule.rb +3 -0
- data/lib/contrast/agent/protect/policy/rule_applicator.rb +12 -0
- data/lib/contrast/agent/protect/rule/base.rb +19 -5
- data/lib/contrast/agent/protect/rule/base_service.rb +6 -0
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +1 -1
- data/lib/contrast/agent/protect/rule/bot_blocker.rb +8 -0
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +8 -0
- data/lib/contrast/agent/protect/rule/deserialization.rb +2 -2
- data/lib/contrast/agent/protect/rule/no_sqli.rb +24 -2
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +1 -1
- data/lib/contrast/agent/protect/rule/path_traversal.rb +8 -0
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +0 -1
- data/lib/contrast/agent/protect/rule/sqli.rb +6 -10
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +6 -2
- data/lib/contrast/agent/protect/rule/unsafe_file_upload.rb +20 -0
- data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +1 -1
- data/lib/contrast/agent/protect/rule/xss.rb +8 -0
- data/lib/contrast/agent/protect/rule/xxe.rb +2 -2
- data/lib/contrast/agent/protect/rule.rb +0 -3
- data/lib/contrast/agent/reporting/attack_result/user_input.rb +0 -1
- data/lib/contrast/agent/reporting/details/details.rb +0 -1
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +12 -0
- data/lib/contrast/agent/reporting/report.rb +1 -0
- data/lib/contrast/agent/reporting/reporter.rb +11 -10
- data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +4 -5
- data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +13 -1
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_activity.rb +20 -5
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample.rb +0 -1
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample_activity.rb +5 -0
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +10 -1
- data/lib/contrast/agent/reporting/reporting_events/application_inventory.rb +2 -1
- data/lib/contrast/agent/reporting/reporting_events/application_reporting_event.rb +10 -0
- data/lib/contrast/agent/reporting/reporting_events/application_settings.rb +40 -0
- data/lib/contrast/agent/reporting/reporting_utilities/ng_response_extractor.rb +137 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +12 -4
- data/lib/contrast/agent/reporting/reporting_utilities/response_extractor.rb +100 -107
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +5 -4
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +97 -63
- data/lib/contrast/agent/reporting/reporting_workers/application_server_worker.rb +46 -0
- data/lib/contrast/agent/reporting/reporting_workers/reporter_heartbeat.rb +51 -0
- data/lib/contrast/agent/reporting/reporting_workers/reporting_workers.rb +14 -0
- data/lib/contrast/agent/reporting/reporting_workers/server_settings_worker.rb +46 -0
- data/lib/contrast/agent/reporting/settings/assess.rb +14 -1
- data/lib/contrast/agent/reporting/settings/assess_rule.rb +18 -0
- data/lib/contrast/agent/reporting/settings/helpers.rb +4 -2
- data/lib/contrast/agent/reporting/settings/protect.rb +17 -12
- data/lib/contrast/agent/reporting/settings/protect_rule.rb +18 -0
- data/lib/contrast/agent/reporting/settings/protect_server_feature.rb +1 -1
- data/lib/contrast/agent/reporting/settings/sensitive_data_masking.rb +1 -1
- data/lib/contrast/agent/reporting/settings/virtual_patch.rb +56 -0
- data/lib/contrast/agent/reporting/settings/virtual_patch_condition.rb +47 -0
- data/lib/contrast/agent/request_context_extend.rb +20 -0
- data/lib/contrast/agent/telemetry/base.rb +11 -10
- data/lib/contrast/agent/telemetry/events/exceptions/obfuscate.rb +108 -103
- data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +1 -1
- data/lib/contrast/agent/thread_watcher.rb +16 -10
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +12 -0
- data/lib/contrast/agent_lib/api/init.rb +1 -7
- data/lib/contrast/agent_lib/api/input_tracing.rb +2 -4
- data/lib/contrast/agent_lib/interface.rb +1 -16
- data/lib/contrast/agent_lib/interface_base.rb +52 -39
- data/lib/contrast/agent_lib/return_types/eval_result.rb +2 -2
- data/lib/contrast/components/assess.rb +26 -4
- data/lib/contrast/components/polling.rb +4 -1
- data/lib/contrast/components/settings.rb +46 -3
- data/lib/contrast/config/config.rb +2 -2
- data/lib/contrast/config/protect_rule_configuration.rb +1 -1
- data/lib/contrast/config/protect_rules_configuration.rb +1 -1
- data/lib/contrast/extension/assess/array.rb +3 -3
- data/lib/contrast/extension/assess/regexp.rb +2 -2
- data/lib/contrast/logger/aliased_logging.rb +48 -15
- data/lib/contrast/utils/input_classification_base.rb +21 -4
- data/lib/contrast/utils/routes_sent.rb +2 -2
- data/lib/contrast/utils/telemetry.rb +1 -1
- data/lib/contrast/utils/telemetry_client.rb +1 -1
- data/resources/protect/policy.json +8 -0
- data/ruby-agent.gemspec +1 -1
- metadata +28 -18
- data/lib/contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification.rb +0 -96
- data/lib/contrast/agent/protect/rule/http_method_tampering.rb +0 -83
- data/lib/contrast/agent/reporting/details/http_method_tempering_details.rb +0 -27
- data/lib/contrast/agent/reporting/reporter_heartbeat.rb +0 -47
- data/lib/contrast/agent/reporting/server_settings_worker.rb +0 -44
- data/lib/contrast/agent_lib/api/method_tempering.rb +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 624b29e40ff797608bb6fef0ab00377cc1bc3e6756af01063d4768fad53bfbfa
|
|
4
|
+
data.tar.gz: 7380498d855e3f6b8a7387ee98351d118b6b70b7bc332536bf1124a870c1a48c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44d0b0cc41d92f58cf048212295fdef8367a4aa4475e3d035c9ae09c80bbcad9de08ba2a63f321381de4af41fd90b581dca2710ef3641d8eb486e8664a3d18cf
|
|
7
|
+
data.tar.gz: cb2f9e2799f8ef7fff7da2ba6714e94022ad5495ddc57c448244c34f649cb70eb756d4f806c1f1274676cbdd37749bf0177cd8539b175ba7b8086857bd1f86a3
|
data/.gitignore
CHANGED
|
@@ -50,7 +50,7 @@ module Contrast
|
|
|
50
50
|
|
|
51
51
|
potential_elements(section, element_start_str).flatten.each do |potential_element|
|
|
52
52
|
next unless potential_element
|
|
53
|
-
next unless element_openings.any? { |opening| potential_element.
|
|
53
|
+
next unless element_openings.any? { |opening| potential_element.start_with?(opening) }
|
|
54
54
|
|
|
55
55
|
section_start = section.index(element_start_str, section_start)
|
|
56
56
|
next unless section_start
|
|
@@ -14,6 +14,7 @@ require 'contrast/utils/telemetry'
|
|
|
14
14
|
require 'contrast/agent/request_handler'
|
|
15
15
|
require 'contrast/agent/static_analysis'
|
|
16
16
|
require 'contrast/agent/telemetry/events/startup_metrics_event'
|
|
17
|
+
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
|
17
18
|
require 'contrast/utils/middleware_utils'
|
|
18
19
|
require 'contrast/utils/reporting/application_activity_batch_utils'
|
|
19
20
|
require 'contrast/utils/timer'
|
|
@@ -23,7 +24,7 @@ module Contrast
|
|
|
23
24
|
# This class allows the Agent to plug into the Rack middleware stack. When the application is first started, we
|
|
24
25
|
# initialize ourselves as a rack middleware inside of #initialize. Afterwards, we process each http request and
|
|
25
26
|
# response as it goes through the middleware stack inside of #call.
|
|
26
|
-
class Middleware
|
|
27
|
+
class Middleware # rubocop:disable Metrics/ClassLength
|
|
27
28
|
include Contrast::Components::Logger::InstanceMethods
|
|
28
29
|
include Contrast::Components::Scope::InstanceMethods
|
|
29
30
|
include Contrast::Utils::MiddlewareUtils
|
|
@@ -171,12 +172,13 @@ module Contrast
|
|
|
171
172
|
with_contrast_scope do
|
|
172
173
|
context.extract_after(response) # update context with final response information
|
|
173
174
|
|
|
174
|
-
# Build and report all collected findings prior response
|
|
175
175
|
Contrast::Agent::FINDINGS.report_collected_findings unless Contrast::Agent::FINDINGS.collection.empty?
|
|
176
176
|
# All protect rules, which are trigger but require response to be reported
|
|
177
177
|
Contrast::Agent::EXPLOITS.report_recorded_exploits(context) unless Contrast::Agent::EXPLOITS.collection.empty?
|
|
178
178
|
# Process Worth Watching Inputs for v2 rules
|
|
179
179
|
Contrast::Agent.worth_watching_analyzer&.add_to_queue(context.agent_input_analysis)
|
|
180
|
+
# Now we can build the ia_results only for postfilter rules.
|
|
181
|
+
context.protect_postfilter_ia
|
|
180
182
|
|
|
181
183
|
if Contrast::Agent.framework_manager.streaming?(env)
|
|
182
184
|
context.reset_activity
|
|
@@ -16,7 +16,6 @@ require 'contrast/agent/protect/rule/path_traversal'
|
|
|
16
16
|
require 'contrast/agent/protect/rule/path_traversal/path_traversal_input_classification'
|
|
17
17
|
require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
|
|
18
18
|
require 'contrast/agent/protect/rule/xss'
|
|
19
|
-
require 'contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification'
|
|
20
19
|
require 'contrast/components/logger'
|
|
21
20
|
require 'contrast/utils/object_share'
|
|
22
21
|
require 'json'
|
|
@@ -29,7 +28,13 @@ module Contrast
|
|
|
29
28
|
module InputAnalyzer
|
|
30
29
|
DISPOSITION_NAME = 'name'
|
|
31
30
|
DISPOSITION_FILENAME = 'filename'
|
|
32
|
-
|
|
31
|
+
PREFILTER_RULES = %w[bot-blocker unsafe-file-upload reflected-xss].cs__freeze
|
|
32
|
+
INFILTER_RULES = %w[
|
|
33
|
+
sql-injection cmd-injection reflected-xss bot-blocker unsafe-file-upload path-traversal
|
|
34
|
+
nosql-injection
|
|
35
|
+
].cs__freeze
|
|
36
|
+
POSTFILTER_RULES = %w[sql-injection cmd-injection reflected-xss path-traversal nosql-injection].cs__freeze
|
|
37
|
+
AGENTLIB_TIMEOUT = 5.cs__freeze
|
|
33
38
|
|
|
34
39
|
class << self
|
|
35
40
|
include Contrast::Agent::Reporting::InputType
|
|
@@ -37,44 +42,8 @@ module Contrast
|
|
|
37
42
|
include Contrast::Utils::ObjectShare
|
|
38
43
|
include Contrast::Components::Logger::InstanceMethods
|
|
39
44
|
|
|
40
|
-
PROTECT_RULES = {
|
|
41
|
-
sqli: {
|
|
42
|
-
rule_name: 'sql-injection',
|
|
43
|
-
classification: Contrast::Agent::Protect::Rule::SqliInputClassification
|
|
44
|
-
},
|
|
45
|
-
cmdi: {
|
|
46
|
-
rule_name: 'cmd-injection',
|
|
47
|
-
classification: Contrast::Agent::Protect::Rule::CmdiInputClassification
|
|
48
|
-
},
|
|
49
|
-
# method_tampering: {
|
|
50
|
-
# rule_name: 'method-tampering',
|
|
51
|
-
# classification: Contrast::Agent::Protect::Rule::HttpMethodTamperingInputClassification
|
|
52
|
-
# },
|
|
53
|
-
reflected_xss: {
|
|
54
|
-
rule_name: Contrast::Agent::Protect::Rule::Xss::NAME,
|
|
55
|
-
classification: Contrast::Agent::Protect::Rule::ReflectedXssInputClassification
|
|
56
|
-
},
|
|
57
|
-
bot_blocker: {
|
|
58
|
-
rule_name: Contrast::Agent::Protect::Rule::BotBlocker::NAME,
|
|
59
|
-
classification: Contrast::Agent::Protect::Rule::BotBlockerInputClassification
|
|
60
|
-
},
|
|
61
|
-
unsafe_file_upload: {
|
|
62
|
-
rule_name: Contrast::Agent::Protect::Rule::UnsafeFileUpload::NAME,
|
|
63
|
-
classification: Contrast::Agent::Protect::Rule::UnsafeFileUploadInputClassification
|
|
64
|
-
},
|
|
65
|
-
path_traversal: {
|
|
66
|
-
rule_name: Contrast::Agent::Protect::Rule::PathTraversal::NAME,
|
|
67
|
-
classification: Contrast::Agent::Protect::Rule::PathTraversalInputClassification
|
|
68
|
-
},
|
|
69
|
-
nosqli: {
|
|
70
|
-
rule_name: Contrast::Agent::Protect::Rule::NoSqli::NAME,
|
|
71
|
-
classification: Contrast::Agent::Protect::Rule::NoSqliInputClassification
|
|
72
|
-
}
|
|
73
|
-
}.cs__freeze
|
|
74
|
-
|
|
75
45
|
# This method with analyze the user input from the context of the
|
|
76
|
-
# current request and
|
|
77
|
-
# found input types
|
|
46
|
+
# current request and return new ia with extracted input types.
|
|
78
47
|
#
|
|
79
48
|
# @param request [Contrast::Agent::Request] current request context.
|
|
80
49
|
# @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
|
|
@@ -87,39 +56,8 @@ module Contrast
|
|
|
87
56
|
|
|
88
57
|
input_analysis = Contrast::Agent::Reporting::InputAnalysis.new
|
|
89
58
|
input_analysis.request = request
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
input_analysis
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
# classify input by rule implementation of worth_watching_v2 for the rules supporting it.
|
|
98
|
-
#
|
|
99
|
-
# @param inputs [String, Array<String>] user input to be analysed.
|
|
100
|
-
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Here we will keep all the results
|
|
101
|
-
# for each protect rule.
|
|
102
|
-
# @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
|
|
103
|
-
def input_classification inputs, input_analysis
|
|
104
|
-
# key = input type, value = user_input
|
|
105
|
-
inputs.each do |input_type, value|
|
|
106
|
-
next if value.nil? || value.empty?
|
|
107
|
-
|
|
108
|
-
PROTECT_RULES.each do |_key, rule|
|
|
109
|
-
protect_rule = Contrast::PROTECT.rule(rule[:rule_name])
|
|
110
|
-
logger.debug("Rule #{ rule[:rule_name] } not recognised in Protect rules") if protect_rule.nil?
|
|
111
|
-
|
|
112
|
-
# check if rule is enabled
|
|
113
|
-
next unless protect_rule&.enabled?
|
|
114
|
-
|
|
115
|
-
# method tampering doesn't take value
|
|
116
|
-
if rule[:rule_name] == Contrast::Agent::Protect::Rule::HttpMethodTampering::NAME
|
|
117
|
-
rule[:classification].send(:classify, rule[:rule_name], input_type, input_analysis)
|
|
118
|
-
else
|
|
119
|
-
rule[:classification].send(:classify, rule[:rule_name], input_type, value, input_analysis)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
59
|
+
# Save those for trigger time
|
|
60
|
+
input_analysis.inputs = extract_input(request)
|
|
123
61
|
input_analysis
|
|
124
62
|
end
|
|
125
63
|
|
|
@@ -146,22 +84,77 @@ module Contrast
|
|
|
146
84
|
inputs
|
|
147
85
|
end
|
|
148
86
|
|
|
87
|
+
# classify input by rule
|
|
88
|
+
#
|
|
89
|
+
# @param rule_id [String] name of the rule
|
|
90
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from
|
|
91
|
+
# analyze method.
|
|
92
|
+
def input_classification_for rule_id, input_analysis
|
|
93
|
+
return unless input_analysis&.inputs
|
|
94
|
+
return unless (protect_rule = Contrast::PROTECT.rule(rule_id)) && protect_rule.enabled?
|
|
95
|
+
|
|
96
|
+
input_analysis.inputs.each do |input_type, value|
|
|
97
|
+
next if value.nil? || value.empty?
|
|
98
|
+
|
|
99
|
+
# append to results.
|
|
100
|
+
protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
|
|
101
|
+
end
|
|
102
|
+
input_analysis
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# classify input by array of rules. There is a timeout for the AgentLib analysis if not set it
|
|
106
|
+
# will use the default 5s.
|
|
107
|
+
#
|
|
108
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Here we will keep all the results
|
|
109
|
+
# for each protect rule.
|
|
110
|
+
# @param prefilter [Boolean] flag to set input analysis for prefilter rules only
|
|
111
|
+
# @param postfilter [Boolean] flag to set input analysis for postfilter rules.
|
|
112
|
+
# @param infilter [Boolean]
|
|
113
|
+
# @param interval [Integer] The timeout determined for the AgentLib analysis to be performed
|
|
114
|
+
# @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
|
|
115
|
+
# @raise [Timeout::Error] If timeout is met.
|
|
116
|
+
def input_classification(input_analysis,
|
|
117
|
+
prefilter: false,
|
|
118
|
+
postfilter: false,
|
|
119
|
+
infilter: false,
|
|
120
|
+
interval: AGENTLIB_TIMEOUT)
|
|
121
|
+
return unless input_analysis
|
|
122
|
+
|
|
123
|
+
rules = if prefilter
|
|
124
|
+
PREFILTER_RULES
|
|
125
|
+
elsif postfilter
|
|
126
|
+
POSTFILTER_RULES
|
|
127
|
+
else
|
|
128
|
+
INFILTER_RULES
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
rules.each do |rule_id|
|
|
132
|
+
# Check to see if rules is already triggered only for infilter:
|
|
133
|
+
next if input_analysis.triggered_rules.include?(rule_id) && infilter
|
|
134
|
+
|
|
135
|
+
Timeout.timeout(interval) do
|
|
136
|
+
input_classification_for(rule_id, input_analysis)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
input_analysis
|
|
140
|
+
rescue Timeout::Error => e
|
|
141
|
+
logger.warn('AgentLib timed out when processing InputAnalysisResult', e, ia_result)
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
149
147
|
# Extract the filename and name of the Content Disposition Header.
|
|
150
148
|
#
|
|
151
149
|
# @param inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
|
|
152
150
|
# @param request [Contrast::Agent::Request] current request context.
|
|
153
151
|
def extract_multipart inputs, request
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
new_pair = elem.strip.split(EQUALS, 2)
|
|
161
|
-
pairs[new_pair[0].downcase] = new_pair[1] if new_pair
|
|
162
|
-
end
|
|
163
|
-
inputs[MULTIPART_NAME] = pairs[DISPOSITION_NAME]
|
|
164
|
-
inputs[MULTIPART_FIELD_NAME] = pairs[DISPOSITION_FILENAME]
|
|
152
|
+
return unless (parsed_data = Rack::Multipart.parse_multipart(request.rack_request.env))
|
|
153
|
+
|
|
154
|
+
filename = parsed_data[DISPOSITION_FILENAME]
|
|
155
|
+
inputs[MULTIPART_FIELD_NAME] = filename[DISPOSITION_FILENAME.to_sym] if filename
|
|
156
|
+
name = filename[DISPOSITION_NAME.to_sym]
|
|
157
|
+
inputs[MULTIPART_NAME] = name if name
|
|
165
158
|
end
|
|
166
159
|
end
|
|
167
160
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'contrast/agent/worker_thread'
|
|
5
|
+
require 'contrast/agent/reporting/input_analysis/input_analysis_result'
|
|
5
6
|
require 'contrast/agent/reporting/input_analysis/score_level'
|
|
6
7
|
require 'contrast/agent/reporting/reporting_events/application_activity'
|
|
7
8
|
require 'contrast/utils/input_classification_base'
|
|
@@ -28,69 +29,73 @@ module Contrast
|
|
|
28
29
|
return if running?
|
|
29
30
|
|
|
30
31
|
@_thread = Contrast::Agent::Thread.new do
|
|
31
|
-
logger.info('Starting
|
|
32
|
+
logger.info('[WorthWatchingAnalyzer] Starting thread.')
|
|
32
33
|
loop do
|
|
33
34
|
sleep(REPORT_INTERVAL_SECOND)
|
|
34
35
|
next if queue.empty?
|
|
35
36
|
|
|
36
37
|
report = false
|
|
37
|
-
|
|
38
|
+
# build attack_results for all infilter active protect rules.
|
|
39
|
+
results = build_results(queue.pop)
|
|
38
40
|
activity = Contrast::Agent::Reporting::ApplicationActivity.new
|
|
39
|
-
|
|
40
|
-
next unless (attack_result = eval_input(
|
|
41
|
+
results.each do |result|
|
|
42
|
+
next unless (attack_result = eval_input(result))
|
|
41
43
|
|
|
42
44
|
activity.attach_defend(attack_result)
|
|
43
45
|
report = true
|
|
44
46
|
end
|
|
45
47
|
Contrast::Agent.reporter.send_event_immediately(activity) if report
|
|
46
48
|
rescue StandardError => e
|
|
47
|
-
logger.debug('
|
|
49
|
+
logger.debug('[WorthWatchingAnalyzer] thread could not process result because of:', e)
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
# param Contrast::Agent::Reporting::InputAnalysis
|
|
54
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
|
|
53
55
|
def add_to_queue input_analysis
|
|
54
|
-
return
|
|
56
|
+
return unless input_analysis
|
|
55
57
|
|
|
56
58
|
if queue.size >= QUEUE_SIZE
|
|
57
|
-
logger.debug('
|
|
59
|
+
logger.debug('[WorthWatchingAnalyzer] queue at max size, skip input_result')
|
|
58
60
|
return
|
|
59
61
|
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
# There will be no results here because of the delay of the protect rule analysis,
|
|
63
|
+
# we need to save the ia which contains the request and saved extracted user inputs to
|
|
64
|
+
# be evaluated on the thread rather than building results here. This way we allow the
|
|
65
|
+
# request to continue and will build the attack results later.
|
|
66
|
+
queue << input_analysis
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
private
|
|
67
70
|
|
|
71
|
+
# This method will build the attack results from the saved ia.
|
|
72
|
+
#
|
|
73
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
|
|
74
|
+
# @return attack_results [array<Contrast::Agent::Reporting::InputAnalysisResult>] all the results
|
|
75
|
+
# from the input analysis.
|
|
76
|
+
def build_results input_analysis
|
|
77
|
+
# Construct the input analysis for the all the infilter rules that were not triggered.
|
|
78
|
+
# There is a set timeout for each rule to be analyzed in. The infilter flag will make
|
|
79
|
+
# sure that if a rule is already triggered during the infilter phase it will not be analyzed
|
|
80
|
+
# now, making sure we don't report same rule twice.
|
|
81
|
+
Contrast::Agent::Protect::InputAnalyzer.input_classification(input_analysis, infilter: true)
|
|
82
|
+
results = []
|
|
83
|
+
input_analysis.results.reject { |val| val.score_level == SCORE_LEVEL::IGNORE }.
|
|
84
|
+
each do |ia_result|
|
|
85
|
+
results << ia_result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
results
|
|
89
|
+
end
|
|
90
|
+
|
|
68
91
|
# @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the WorthWatching InputAnalysisResult
|
|
69
|
-
# @return [Contrast::Agent::Reporting::
|
|
92
|
+
# @return [Contrast::Agent::Reporting::AttackResult, nil] InputAnalysisResult updated Result or nil
|
|
70
93
|
def eval_input ia_result
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
if ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD
|
|
74
|
-
logger.debug("Skip anaylsis: Input size is larger than #{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
|
|
75
|
-
return
|
|
76
|
-
end
|
|
94
|
+
return build_attack_result(ia_result) unless ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
convert_input_type(ia_result.input_type),
|
|
82
|
-
Contrast::AGENT_LIB.rule_set[ia_result.rule_id],
|
|
83
|
-
Contrast::AGENT_LIB.eval_option[:NONE])
|
|
84
|
-
end
|
|
85
|
-
score = input_eval&.score || 0
|
|
86
|
-
return if score <= THRESHOLD
|
|
87
|
-
|
|
88
|
-
ia_result.score_level = DEFINITEATTACK
|
|
89
|
-
build_attack_result(ia_result)
|
|
90
|
-
rescue Timeout::Error => e
|
|
91
|
-
logger.warn('AgentLib timed out when processing WORTHWATCHING InputAnalysisResult', e, ia_result)
|
|
92
|
-
nil
|
|
93
|
-
end
|
|
96
|
+
logger.debug("[WorthWatchingAnalyzer] Skipping analysis: Input size is larger than
|
|
97
|
+
#{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
|
|
98
|
+
nil
|
|
94
99
|
end
|
|
95
100
|
|
|
96
101
|
# @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the updated InputAnalysisResult
|
|
@@ -32,6 +32,8 @@ module Contrast
|
|
|
32
32
|
|
|
33
33
|
clazz = object.is_a?(Module) ? object : object.cs__class
|
|
34
34
|
class_name = clazz.cs__name
|
|
35
|
+
# Get the ia for current rule:
|
|
36
|
+
apply_classification(rule_name, Contrast::Agent::REQUEST_TRACKER.current)
|
|
35
37
|
rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, class_name, method, command)
|
|
36
38
|
# invoke cmdi sub-rules.
|
|
37
39
|
rule.sub_rules.each do |sub_rule|
|
|
@@ -20,6 +20,8 @@ module Contrast
|
|
|
20
20
|
return unless valid_input?(args)
|
|
21
21
|
return if skip_analysis?
|
|
22
22
|
|
|
23
|
+
# Get the ia for current rule:
|
|
24
|
+
apply_classification(rule_name, Contrast::Agent::REQUEST_TRACKER.current)
|
|
23
25
|
database = properties['database']
|
|
24
26
|
operations = args[0]
|
|
25
27
|
context = Contrast::Agent::REQUEST_TRACKER.current
|
|
@@ -48,10 +50,11 @@ module Contrast
|
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def handle_operation context, database, _action, operation
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
# TODO: RUBY-1991 Expand NoSQLI triggers
|
|
54
|
+
# data = extract_mongo_data(operation)
|
|
55
|
+
# return unless data && !data.empty?
|
|
53
56
|
|
|
54
|
-
rule.infilter(context, database,
|
|
57
|
+
rule.infilter(context, database, operation)
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def extract_mongo_data operation
|
|
@@ -29,6 +29,9 @@ module Contrast
|
|
|
29
29
|
write_marker = write?(action, *args)
|
|
30
30
|
possible_write = write_marker && possible_write?(write_marker)
|
|
31
31
|
|
|
32
|
+
# Get the ia for current rule:
|
|
33
|
+
apply_classification(rule_name, Contrast::Agent::REQUEST_TRACKER.current)
|
|
34
|
+
|
|
32
35
|
# Invoke semantic rules from here, not in a separate protect policy
|
|
33
36
|
invoke_semantic_rules(path, possible_write, object, method)
|
|
34
37
|
path_traversal_rule(path, possible_write, object, method)
|
|
@@ -29,6 +29,9 @@ module Contrast
|
|
|
29
29
|
return if skip_analysis?
|
|
30
30
|
|
|
31
31
|
sql = args[index]
|
|
32
|
+
|
|
33
|
+
# Get the ia for current rule:
|
|
34
|
+
apply_classification(rule_name, Contrast::Agent::REQUEST_TRACKER.current)
|
|
32
35
|
rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, database, sql)
|
|
33
36
|
rule.sub_rules.each { |sub_rule| sub_rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, sql) }
|
|
34
37
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'contrast/components/logger'
|
|
5
|
+
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
|
5
6
|
|
|
6
7
|
module Contrast
|
|
7
8
|
module Agent
|
|
@@ -44,6 +45,17 @@ module Contrast
|
|
|
44
45
|
rule: rule_name)
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
# applies input_analysis for the invoked rule
|
|
49
|
+
#
|
|
50
|
+
# @param rule_id [String] name of the rule
|
|
51
|
+
# @param context [Contrast::Agent::RequestContext] current request contest
|
|
52
|
+
def apply_classification rule_id, context
|
|
53
|
+
return unless context
|
|
54
|
+
return unless (ia = context.agent_input_analysis)
|
|
55
|
+
|
|
56
|
+
Contrast::Agent::Protect::InputAnalyzer.input_classification_for(rule_id, ia)
|
|
57
|
+
end
|
|
58
|
+
|
|
47
59
|
protected
|
|
48
60
|
|
|
49
61
|
# Calls the actual rule for this applicator, if required. Most rules
|
|
@@ -5,6 +5,7 @@ require 'contrast/components/logger'
|
|
|
5
5
|
require 'contrast/components/scope'
|
|
6
6
|
require 'contrast/utils/object_share'
|
|
7
7
|
require 'contrast/agent/reporting/attack_result/response_type'
|
|
8
|
+
require 'contrast/agent/reporting/attack_result/attack_result'
|
|
8
9
|
|
|
9
10
|
module Contrast
|
|
10
11
|
module Agent
|
|
@@ -51,6 +52,23 @@ module Contrast
|
|
|
51
52
|
Contrast::Utils::ObjectShare::EMPTY_ARRAY
|
|
52
53
|
end
|
|
53
54
|
|
|
55
|
+
# The classification module used for each specific rule to
|
|
56
|
+
# classify input data and score it. Extend for each rule.
|
|
57
|
+
def classification; end
|
|
58
|
+
|
|
59
|
+
# Input Classification stage is done to determine if an user input is
|
|
60
|
+
# DEFINITEATTACK or to be ignored.
|
|
61
|
+
#
|
|
62
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
|
63
|
+
# @param value [Hash<String>] the value of the input.
|
|
64
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
|
|
65
|
+
# agent analysis from the current
|
|
66
|
+
# Request.
|
|
67
|
+
# @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
|
|
68
|
+
def classify input_type, value, input_analysis
|
|
69
|
+
classification.classify(rule_name, input_type, value, input_analysis)
|
|
70
|
+
end
|
|
71
|
+
|
|
54
72
|
def enabled?
|
|
55
73
|
# 1. it is not enabled because protect is not enabled
|
|
56
74
|
return false unless ::Contrast::AGENT.enabled?
|
|
@@ -288,11 +306,7 @@ module Contrast
|
|
|
288
306
|
# @param _context [Contrast::Agent::RequestContext] the context of
|
|
289
307
|
# the current request
|
|
290
308
|
# @return [Contrast::Agent::Reporting::AttackResult]
|
|
291
|
-
def build_attack_result _context
|
|
292
|
-
result = Contrast::Agent::Reporting::AttackResult.new
|
|
293
|
-
result.rule_id = rule_name
|
|
294
|
-
result
|
|
295
|
-
end
|
|
309
|
+
def build_attack_result _context; end
|
|
296
310
|
|
|
297
311
|
# @param sample [Contrast::Agent::Reporting::RaspRuleSample]
|
|
298
312
|
# @param result [Contrast::Agent::Reporting::AttackResult, nil] previous attack result for this rule, if one
|
|
@@ -96,6 +96,12 @@ module Contrast
|
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
def build_attack_result _context
|
|
100
|
+
result = Contrast::Agent::Reporting::AttackResult.new
|
|
101
|
+
result.rule_id = rule_name
|
|
102
|
+
result
|
|
103
|
+
end
|
|
104
|
+
|
|
99
105
|
# @param context [Contrast::Agent::RequestContext]
|
|
100
106
|
# @param potential_attack_string [String, nil]
|
|
101
107
|
# @param **kwargs
|
|
@@ -6,6 +6,7 @@ require 'contrast/components/logger'
|
|
|
6
6
|
require 'contrast/agent/reporting/input_analysis/input_type'
|
|
7
7
|
require 'contrast/agent/reporting/input_analysis/score_level'
|
|
8
8
|
require 'contrast/agent_lib/interface'
|
|
9
|
+
require 'contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification'
|
|
9
10
|
|
|
10
11
|
module Contrast
|
|
11
12
|
module Agent
|
|
@@ -27,6 +28,13 @@ module Contrast
|
|
|
27
28
|
APPLICABLE_USER_INPUTS
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
# Bot blocker input classification
|
|
32
|
+
#
|
|
33
|
+
# @return [module<Contrast::Agent::Protect::Rule::BotBlockerInputClassification>]
|
|
34
|
+
def classification
|
|
35
|
+
@_classification ||= Contrast::Agent::Protect::Rule::BotBlockerInputClassification.cs__freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
# BotBlocker prefilter:
|
|
31
39
|
#
|
|
32
40
|
# @param context [Contrast::Agent::RequestContext] current request contest
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require 'contrast/agent/reporting/details/cmd_injection_details'
|
|
5
5
|
require 'contrast/agent/reporting/attack_result/user_input'
|
|
6
|
+
require 'contrast/agent/protect/rule/cmdi/cmdi_input_classification'
|
|
6
7
|
|
|
7
8
|
module Contrast
|
|
8
9
|
module Agent
|
|
@@ -21,6 +22,13 @@ module Contrast
|
|
|
21
22
|
MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
|
|
22
23
|
].cs__freeze
|
|
23
24
|
|
|
25
|
+
# CMDI input classification
|
|
26
|
+
#
|
|
27
|
+
# @return [module<Contrast::Agent::Protect::Rule::CmdiInputClassification>]
|
|
28
|
+
def classification
|
|
29
|
+
@_classification ||= Contrast::Agent::Protect::Rule::CmdiInputClassification.cs__freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
24
32
|
# CMDI infilter:
|
|
25
33
|
#
|
|
26
34
|
# @param context [Contrast::Agent::RequestContext] current request contest
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require 'contrast/agent/protect/rule/
|
|
4
|
+
require 'contrast/agent/protect/rule/base_service'
|
|
5
5
|
require 'contrast/agent/reporting/details/untrusted_deserialization_details'
|
|
6
6
|
require 'contrast/components/logger'
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ module Contrast
|
|
|
11
11
|
module Rule
|
|
12
12
|
# This class handles our implementation of the Untrusted
|
|
13
13
|
# Deserialization Protect rule.
|
|
14
|
-
class Deserialization < Contrast::Agent::Protect::Rule::
|
|
14
|
+
class Deserialization < Contrast::Agent::Protect::Rule::BaseService
|
|
15
15
|
# The TeamServer recognized name of this rule
|
|
16
16
|
include Contrast::Components::Logger::InstanceMethods
|
|
17
17
|
# Used to name this input since input analysis isn't done for this
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require 'contrast/agent/protect/rule/base_service'
|
|
5
5
|
require 'contrast/agent/protect/rule/sql_sample_builder'
|
|
6
6
|
require 'contrast/agent/reporting/input_analysis/input_type'
|
|
7
|
+
require 'contrast/agent/protect/rule/no_sqli/no_sqli_input_classification'
|
|
7
8
|
|
|
8
9
|
module Contrast
|
|
9
10
|
module Agent
|
|
@@ -39,6 +40,13 @@ module Contrast
|
|
|
39
40
|
APPLICABLE_USER_INPUTS
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
# NoSQLI input classification
|
|
44
|
+
#
|
|
45
|
+
# @return [module<Contrast::Agent::Protect::Rule::NoSqliInputClassification>]
|
|
46
|
+
def classification
|
|
47
|
+
@_classification ||= Contrast::Agent::Protect::Rule::NoSqliInputClassification.cs__freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
# @raise [Contrast::SecurityException] if the attack is blocked
|
|
43
51
|
# raised with BLOCK_MESSAGE
|
|
44
52
|
def infilter context, database, query_string
|
|
@@ -78,9 +86,14 @@ module Contrast
|
|
|
78
86
|
|
|
79
87
|
def find_attacker context, potential_attack_string, **kwargs
|
|
80
88
|
if potential_attack_string
|
|
81
|
-
# We need the query hash to be a JSON string to match on JSON input attacks
|
|
89
|
+
# We need the query hash to be a JSON string to match on JSON input attacks.
|
|
90
|
+
# Before that we need to check if a string is already in json form.
|
|
82
91
|
begin
|
|
83
|
-
potential_attack_string =
|
|
92
|
+
potential_attack_string = if json?(potential_attack_string)
|
|
93
|
+
potential_attack_string
|
|
94
|
+
else
|
|
95
|
+
JSON.generate(potential_attack_string).to_s
|
|
96
|
+
end
|
|
84
97
|
rescue JSON::GeneratorError
|
|
85
98
|
logger.trace('Error in JSON::generate', input: potential_attack_string)
|
|
86
99
|
nil
|
|
@@ -88,6 +101,15 @@ module Contrast
|
|
|
88
101
|
end
|
|
89
102
|
super(context, potential_attack_string, **kwargs)
|
|
90
103
|
end
|
|
104
|
+
|
|
105
|
+
# Check to see if a string is in JSON form.
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean]
|
|
108
|
+
def json? string
|
|
109
|
+
return true if JSON.parse(string)
|
|
110
|
+
rescue StandardError
|
|
111
|
+
false
|
|
112
|
+
end
|
|
91
113
|
end
|
|
92
114
|
end
|
|
93
115
|
end
|