contrast-agent 6.9.0 → 6.11.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/ext/build_funchook.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/split.rb +1 -4
- data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -1
- data/lib/contrast/agent/middleware.rb +5 -3
- data/lib/contrast/agent/patching/policy/method_policy_extend.rb +6 -2
- data/lib/contrast/agent/patching/policy/trigger_node.rb +1 -1
- 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 +5 -2
- 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 +7 -2
- 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_backdoors.rb +1 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +9 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_chained_command.rb +1 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_dangerous_path.rb +1 -1
- 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/postgres_sql_scanner.rb +0 -1
- 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 +12 -15
- 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_events/discovered_route.rb +9 -5
- data/lib/contrast/agent/reporting/reporting_events/observed_route.rb +8 -5
- data/lib/contrast/agent/reporting/reporting_utilities/endpoints.rb +7 -7
- 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 +101 -67
- 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 +13 -15
- 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/config.rb +1 -1
- 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/framework/rack/patch/session_cookie.rb +2 -1
- data/lib/contrast/logger/aliased_logging.rb +48 -15
- data/lib/contrast/utils/duck_utils.rb +18 -0
- data/lib/contrast/utils/heap_dump_util.rb +1 -1
- data/lib/contrast/utils/input_classification_base.rb +21 -4
- data/lib/contrast/utils/log_utils.rb +1 -1
- data/lib/contrast/utils/middleware_utils.rb +1 -1
- data/lib/contrast/utils/patching/policy/patch_utils.rb +2 -2
- data/lib/contrast/utils/routes_sent.rb +6 -2
- data/lib/contrast/utils/telemetry.rb +2 -2
- data/lib/contrast/utils/telemetry_client.rb +1 -1
- data/resources/protect/policy.json +8 -0
- data/ruby-agent.gemspec +6 -6
- metadata +40 -30
- 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: fddcf57afedf79d84cacef08c959a190f29f6db9248d34d1a01c3abe1b13553b
|
|
4
|
+
data.tar.gz: 599882cd812552c2cc12c86f6ecd526e7a7d1e458cf8ea4d961d17ef5179c9ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e91dd42c9aa9f9cb934bfe878e0f6be951f9397978c874dd2a4b38c0a78f1cfe6b7e957ec6abaa3bb97a194f6494950f9ec93ebad408cf2767df06b1475bd373
|
|
7
|
+
data.tar.gz: ad17804c1abf24410ba2bbe7798ce142eba284cb0911830e40f6f92917a47961d4cfaefa79ed8affb8d1d821e27a85f57ace126a64417f6fe672f965916c3dba
|
data/.gitignore
CHANGED
data/ext/build_funchook.rb
CHANGED
|
@@ -17,7 +17,7 @@ unless find_header('funchook.h', ext_path)
|
|
|
17
17
|
extension_paths = Dir[contrast_gem_dir_search]
|
|
18
18
|
extension_paths.map! do |extension_path|
|
|
19
19
|
target_path = File.join(extension_path, 'shared_libraries')
|
|
20
|
-
FileUtils.mkdir_p(target_path)
|
|
20
|
+
FileUtils.mkdir_p(target_path)
|
|
21
21
|
target_path
|
|
22
22
|
end
|
|
23
23
|
bundler_install_target_paths += extension_paths
|
|
@@ -17,12 +17,11 @@ module Contrast
|
|
|
17
17
|
module Propagator
|
|
18
18
|
# This class is specifically for String#split & String#grapheme_clusters propagation
|
|
19
19
|
# it propagates tag ranges from a string to elements within an untracked array
|
|
20
|
-
class Split < Contrast::Agent::Assess::Policy::Propagator::Base
|
|
20
|
+
class Split < Contrast::Agent::Assess::Policy::Propagator::Base # rubocop:disable Metrics/ClassLength
|
|
21
21
|
extend Contrast::Components::Scope::InstanceMethods
|
|
22
22
|
extend Contrast::Components::Logger::InstanceMethods
|
|
23
23
|
extend Contrast::Utils::Assess::SplitUtils
|
|
24
24
|
extend Contrast::Utils::Assess::EventLimitUtils
|
|
25
|
-
|
|
26
25
|
SPLIT_TRACKER = Contrast::Utils::ThreadTracker.new
|
|
27
26
|
|
|
28
27
|
class << self
|
|
@@ -61,7 +60,6 @@ module Contrast
|
|
|
61
60
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
62
61
|
logger.warn('Unable to record split context', e)
|
|
63
62
|
end
|
|
64
|
-
|
|
65
63
|
yield
|
|
66
64
|
ensure
|
|
67
65
|
# String#split exit. Remove propagation context.
|
|
@@ -158,7 +156,6 @@ module Contrast
|
|
|
158
156
|
# Get tags for element from source by element range.
|
|
159
157
|
range = current_index...(current_index + target_elem.length)
|
|
160
158
|
tags = source_properties.tags_at_range(range)
|
|
161
|
-
|
|
162
159
|
# Set element properties accordingly.
|
|
163
160
|
elem_properties.clear_tags
|
|
164
161
|
tags.each_pair { |key, value| elem_properties.set_tags(key, value) }
|
|
@@ -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
|
|
@@ -43,7 +44,7 @@ module Contrast
|
|
|
43
44
|
@app = app # THIS MUST BE FIRST AND ALWAYS SET!
|
|
44
45
|
setup_agent # THIS MUST BE SECOND AND ALWAYS CALLED!
|
|
45
46
|
unless ::Contrast::AGENT.enabled?
|
|
46
|
-
logger.error('The Agent was unable to initialize before the application middleware was initialized. '\
|
|
47
|
+
logger.error('The Agent was unable to initialize before the application middleware was initialized. ' \
|
|
47
48
|
'Disabling permanently.')
|
|
48
49
|
::Contrast::AGENT.disable! # ensure the agent is disabled (probably redundant)
|
|
49
50
|
return
|
|
@@ -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
|
|
@@ -57,8 +57,12 @@ module Contrast
|
|
|
57
57
|
nodes.find { |node| node }&.method_visibility
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def check_method_policy_nodes_empty?(source_node,
|
|
61
|
-
|
|
60
|
+
def check_method_policy_nodes_empty?(source_node,
|
|
61
|
+
propagation_node,
|
|
62
|
+
trigger_node,
|
|
63
|
+
protect_node,
|
|
64
|
+
inventory_node,
|
|
65
|
+
deadzone_node)
|
|
62
66
|
return false unless source_node.nil? && propagation_node.nil? && trigger_node.nil? && protect_node.nil? &&
|
|
63
67
|
inventory_node.nil? && deadzone_node.nil?
|
|
64
68
|
|
|
@@ -46,7 +46,7 @@ module Contrast
|
|
|
46
46
|
super
|
|
47
47
|
unless applicator.public_methods(false).any?(applicator_method)
|
|
48
48
|
raise(ArgumentError,
|
|
49
|
-
"#{ id } did not have a proper applicator method: "\
|
|
49
|
+
"#{ id } did not have a proper applicator method: " \
|
|
50
50
|
"#{ applicator } does not respond to #{ applicator_method }. Unable to create.")
|
|
51
51
|
end
|
|
52
52
|
validate_properties
|
|
@@ -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)
|
|
@@ -98,10 +101,10 @@ module Contrast
|
|
|
98
101
|
CS__SAFER_REL_PATHS = %w[public app log tmp].cs__freeze
|
|
99
102
|
def safer_abs_paths
|
|
100
103
|
@_safer_abs_paths ||= begin
|
|
101
|
-
pwd = ENV
|
|
104
|
+
pwd = ENV.fetch('PWD', nil)
|
|
102
105
|
if pwd
|
|
103
106
|
tmp = CS__SAFER_REL_PATHS.map { |r| "#{ pwd }/#{ r }" }
|
|
104
|
-
gems = ENV
|
|
107
|
+
gems = ENV.fetch('GEM_PATH', nil)
|
|
105
108
|
tmp += gems.split(Contrast::Utils::ObjectShare::COLON) if gems
|
|
106
109
|
tmp.map!(&:downcase)
|
|
107
110
|
tmp
|
|
@@ -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
|
|
@@ -91,11 +91,16 @@ module Contrast
|
|
|
91
91
|
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless context&.agent_input_analysis&.results
|
|
92
92
|
|
|
93
93
|
context.agent_input_analysis.results.select do |ia_result|
|
|
94
|
-
ia_result.rule_id == rule_name &&
|
|
95
|
-
ia_result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE
|
|
94
|
+
ia_result.rule_id == rule_name && ia_result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE
|
|
96
95
|
end
|
|
97
96
|
end
|
|
98
97
|
|
|
98
|
+
def build_attack_result _context
|
|
99
|
+
result = Contrast::Agent::Reporting::AttackResult.new
|
|
100
|
+
result.rule_id = rule_name
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
|
|
99
104
|
# @param context [Contrast::Agent::RequestContext]
|
|
100
105
|
# @param potential_attack_string [String, nil]
|
|
101
106
|
# @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
|