contrast-agent 4.9.0 → 4.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rspec_parallel +6 -0
- data/ext/cs__common/cs__common.c +19 -7
- data/ext/cs__common/cs__common.h +4 -2
- data/ext/cs__contrast_patch/cs__contrast_patch.c +32 -11
- data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -4
- data/lib/contrast/agent/assess/contrast_event.rb +1 -2
- data/lib/contrast/agent/assess/contrast_object.rb +1 -4
- data/lib/contrast/agent/assess/finalizers/hash.rb +0 -1
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
- data/lib/contrast/agent/assess/policy/patcher.rb +0 -1
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -2
- data/lib/contrast/agent/assess/policy/preshift.rb +29 -12
- data/lib/contrast/agent/assess/policy/propagation_method.rb +100 -57
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -2
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
- data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
- data/lib/contrast/agent/assess/policy/propagator/split.rb +3 -2
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +1 -0
- data/lib/contrast/agent/assess/policy/rewriter_patch.rb +0 -1
- data/lib/contrast/agent/assess/policy/source_method.rb +13 -17
- data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -1
- data/lib/contrast/agent/assess/policy/trigger_method.rb +60 -85
- data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
- data/lib/contrast/agent/assess/property/evented.rb +2 -1
- data/lib/contrast/agent/assess/property/tagged.rb +34 -25
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +0 -1
- data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
- data/lib/contrast/agent/disable_reaction.rb +1 -1
- data/lib/contrast/agent/exclusion_matcher.rb +0 -4
- data/lib/contrast/agent/inventory/database_config.rb +117 -0
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +6 -5
- data/lib/contrast/agent/inventory/policy/datastores.rb +2 -2
- data/lib/contrast/agent/middleware.rb +1 -0
- data/lib/contrast/agent/patching/policy/after_load_patch.rb +3 -0
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +17 -12
- data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
- data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
- data/lib/contrast/agent/patching/policy/patch.rb +17 -6
- data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
- data/lib/contrast/agent/patching/policy/patcher.rb +9 -9
- data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
- data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
- data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
- data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
- data/lib/contrast/agent/reaction_processor.rb +1 -1
- data/lib/contrast/agent/request.rb +9 -4
- data/lib/contrast/agent/request_context.rb +51 -33
- data/lib/contrast/agent/rule_set.rb +2 -4
- data/lib/contrast/agent/scope.rb +32 -20
- data/lib/contrast/agent/static_analysis.rb +1 -1
- data/lib/contrast/agent/tracepoint_hook.rb +16 -3
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +0 -1
- data/lib/contrast/api/communication/messaging_queue.rb +12 -6
- data/lib/contrast/api/communication/service_lifecycle.rb +4 -1
- data/lib/contrast/api/communication/socket_client.rb +4 -4
- data/lib/contrast/api/decorators/agent_startup.rb +4 -4
- data/lib/contrast/api/decorators/application_startup.rb +6 -5
- data/lib/contrast/api/decorators/route_coverage.rb +24 -1
- data/lib/contrast/components/agent.rb +5 -2
- data/lib/contrast/components/assess.rb +13 -3
- data/lib/contrast/components/base.rb +2 -2
- data/lib/contrast/components/config.rb +1 -0
- data/lib/contrast/components/contrast_service.rb +4 -2
- data/lib/contrast/components/logger.rb +13 -8
- data/lib/contrast/components/scope.rb +9 -28
- data/lib/contrast/config/assess_configuration.rb +1 -0
- data/lib/contrast/config/base_configuration.rb +14 -6
- data/lib/contrast/configuration.rb +19 -15
- data/lib/contrast/extension/assess/array.rb +1 -11
- data/lib/contrast/extension/assess/eval_trigger.rb +0 -20
- data/lib/contrast/extension/assess/fiber.rb +0 -11
- data/lib/contrast/extension/assess/hash.rb +0 -10
- data/lib/contrast/extension/assess/kernel.rb +1 -10
- data/lib/contrast/extension/assess/marshal.rb +3 -11
- data/lib/contrast/extension/assess/regexp.rb +0 -11
- data/lib/contrast/extension/assess/string.rb +1 -26
- data/lib/contrast/extension/extension.rb +61 -0
- data/lib/contrast/framework/grape/support.rb +174 -0
- data/lib/contrast/framework/manager.rb +42 -6
- data/lib/contrast/framework/rack/support.rb +1 -1
- data/lib/contrast/framework/rails/patch/assess_configuration.rb +0 -1
- data/lib/contrast/framework/rails/patch/support.rb +6 -3
- data/lib/contrast/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/active_record_named.rb +1 -0
- data/lib/contrast/framework/rails/support.rb +60 -13
- data/lib/contrast/framework/sinatra/support.rb +1 -1
- data/lib/contrast/logger/log.rb +89 -15
- data/lib/contrast/tasks/config.rb +0 -1
- data/lib/contrast/utils/class_util.rb +58 -44
- data/lib/contrast/utils/io_util.rb +43 -35
- data/lib/contrast/utils/lru_cache.rb +45 -0
- data/lib/contrast/utils/ruby_ast_rewriter.rb +16 -13
- data/lib/contrast/utils/tag_util.rb +2 -1
- data/lib/contrast.rb +1 -1
- data/resources/assess/policy.json +208 -7
- data/resources/deadzone/policy.json +91 -0
- data/ruby-agent.gemspec +10 -2
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +74 -26
- data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
- data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
- data/ext/cs__protect_kernel/extconf.rb +0 -5
- data/lib/contrast/extension/protect/kernel.rb +0 -39
- data/lib/contrast/utils/inventory_util.rb +0 -113
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'contrast/agent/protect/rule/base_service'
|
5
|
+
require 'contrast/agent/protect/rule/sql_sample_builder'
|
5
6
|
|
6
7
|
module Contrast
|
7
8
|
module Agent
|
@@ -9,6 +10,12 @@ module Contrast
|
|
9
10
|
module Rule
|
10
11
|
# The Ruby implementation of the Protect NoSQL Injection rule.
|
11
12
|
class NoSqli < Contrast::Agent::Protect::Rule::BaseService
|
13
|
+
# Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering
|
14
|
+
# by TeamServer
|
15
|
+
include SqlSampleBuilder::NoSqliSample
|
16
|
+
# Defining build_attack_with_match method
|
17
|
+
include SqlSampleBuilder::AttackBuilder
|
18
|
+
|
12
19
|
NAME = 'nosql-injection'
|
13
20
|
BLOCK_MESSAGE = 'NoSQLi rule triggered. Response blocked.'
|
14
21
|
|
@@ -31,40 +38,6 @@ module Contrast
|
|
31
38
|
raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
|
32
39
|
end
|
33
40
|
|
34
|
-
def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
|
35
|
-
if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
|
36
|
-
mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
|
37
|
-
|
38
|
-
return result
|
39
|
-
end
|
40
|
-
|
41
|
-
attack_string = input_analysis_result.value
|
42
|
-
regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
|
43
|
-
return unless query_string.match?(regexp)
|
44
|
-
|
45
|
-
scanner = select_scanner
|
46
|
-
ss = StringScanner.new(query_string)
|
47
|
-
length = attack_string.length
|
48
|
-
while ss.scan_until(regexp)
|
49
|
-
# the pos of StringScanner is at the end of the regexp (input string),
|
50
|
-
# we need the beginning
|
51
|
-
idx = ss.pos - attack_string.length
|
52
|
-
last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
|
53
|
-
next unless last_boundary && boundary
|
54
|
-
|
55
|
-
kwargs[:start_idx] = idx
|
56
|
-
kwargs[:end_idx] = idx + length
|
57
|
-
kwargs[:boundary_overrun_idx] = boundary
|
58
|
-
kwargs[:input_boundary_idx] = last_boundary
|
59
|
-
|
60
|
-
result ||= build_attack_result(context)
|
61
|
-
update_successful_attack_response(context, input_analysis_result, result, query_string)
|
62
|
-
append_sample(context, input_analysis_result, result, query_string, **kwargs)
|
63
|
-
end
|
64
|
-
|
65
|
-
result
|
66
|
-
end
|
67
|
-
|
68
41
|
protected
|
69
42
|
|
70
43
|
def find_attacker context, potential_attack_string, **kwargs
|
@@ -79,25 +52,6 @@ module Contrast
|
|
79
52
|
end
|
80
53
|
super(context, potential_attack_string, **kwargs)
|
81
54
|
end
|
82
|
-
|
83
|
-
def build_sample context, input_analysis_result, candidate_string, **kwargs
|
84
|
-
input = input_analysis_result.value
|
85
|
-
|
86
|
-
sample = build_base_sample(context, input_analysis_result)
|
87
|
-
sample.no_sqli = Contrast::Api::Dtm::NoSqlInjectionDetails.new
|
88
|
-
sample.no_sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
|
89
|
-
sample.no_sqli.start_idx = sample.no_sqli.query.index(input).to_i
|
90
|
-
sample.no_sqli.boundary_overrun_idx = sample.no_sqli.start_idx + input.length
|
91
|
-
sample.no_sqli.input_boundary_idx = kwargs[:boundary].to_i
|
92
|
-
sample.no_sqli.input_boundary_idx = kwargs[:last_boundary].to_i
|
93
|
-
sample
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
def select_scanner
|
99
|
-
@_select_scanner ||= Contrast::Agent::Protect::Rule::NoSqli::MongoNoSqlScanner.new
|
100
|
-
end
|
101
55
|
end
|
102
56
|
end
|
103
57
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# Copyright (c) 2021 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/base'
|
5
|
+
require 'contrast/agent/protect/rule/base_service'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
module Protect
|
10
|
+
module Rule
|
11
|
+
module SqlSampleBuilder
|
12
|
+
# Generate a sample for the SQL injection detection rule, allowing for reporting to and rendering
|
13
|
+
# by TeamServer
|
14
|
+
#
|
15
|
+
# @param context [Contrast::Agent::RequestContext] the context for the current request
|
16
|
+
# @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
|
17
|
+
# if one exists, in the case of multiple inputs being found to violate the protection criteria
|
18
|
+
# @candidate_string [String] the value of the input which may be an attack
|
19
|
+
# @kwargs [Hash] key - value pairs of context individual rules need to build out details
|
20
|
+
# to send to the Service to tell the story of the attack
|
21
|
+
# @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack
|
22
|
+
module SqliSample
|
23
|
+
def build_sample context, input_analysis_result, candidate_string, **kwargs
|
24
|
+
sqli_sample = build_base_sample(context, input_analysis_result)
|
25
|
+
sqli_sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
|
26
|
+
sqli_sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
|
27
|
+
sqli_sample.sqli.start_idx = kwargs[:start_idx]
|
28
|
+
sqli_sample.sqli.end_idx = kwargs[:end_idx]
|
29
|
+
sqli_sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
|
30
|
+
sqli_sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
|
31
|
+
sqli_sample
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering
|
36
|
+
# by TeamServer
|
37
|
+
#
|
38
|
+
# @param context [Contrast::Agent::RequestContext] the context for the current request
|
39
|
+
# @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
|
40
|
+
# if one exists, in the case of multiple inputs being found to violate the protection criteria
|
41
|
+
# @candidate_string [String] the value of the input which may be an attack
|
42
|
+
# @kwargs [Hash] key - value pairs of context individual rules need to build out details
|
43
|
+
# to send to the Service to tell the story of the attack
|
44
|
+
# @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack
|
45
|
+
module NoSqliSample
|
46
|
+
def build_sample context, input_analysis_result, candidate_string, **kwargs
|
47
|
+
no_sqli_sample = build_base_sample(context, input_analysis_result)
|
48
|
+
no_sqli_sample.no_sqli = Contrast::Api::Dtm::NoSqlInjectionDetails.new
|
49
|
+
no_sqli_sample.no_sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
|
50
|
+
no_sqli_sample.no_sqli.start_idx = kwargs[:start_idx].to_i
|
51
|
+
no_sqli_sample.no_sqli.end_idx = kwargs[:end_idx].to_i
|
52
|
+
no_sqli_sample.no_sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
|
53
|
+
no_sqli_sample.no_sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
|
54
|
+
no_sqli_sample
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# This Module is how we apply the attack fo NoSQL and SQL Injection rule.
|
59
|
+
# It includes methods for building attack with match and database scanners
|
60
|
+
module AttackBuilder
|
61
|
+
# Set up an attack result and assigns Database scanner for the No-SQL and SQLI injection detection rules
|
62
|
+
#
|
63
|
+
# @param context [Contrast::Agent::RequestContext] the context for the current request
|
64
|
+
# @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
|
65
|
+
# if one exists, in the case of multiple inputs being found to violate the protection criteria
|
66
|
+
# @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists,
|
67
|
+
# in the case of multiple inputs being found to violate the protection criteria
|
68
|
+
# @query_string [string] he value of the input which may be an attack
|
69
|
+
# @kwargs [Hash] key - value pairs of context individual rules need to build out details to send
|
70
|
+
# to the Service to tell the story of the attack
|
71
|
+
# @return [Contrast::Api::Dtm::AttackResult] the result from this attack
|
72
|
+
def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
|
73
|
+
if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
|
74
|
+
mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
|
75
|
+
|
76
|
+
return result
|
77
|
+
end
|
78
|
+
|
79
|
+
attack_string = input_analysis_result.value
|
80
|
+
regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
|
81
|
+
|
82
|
+
return unless query_string.match?(regexp)
|
83
|
+
|
84
|
+
database = kwargs[:database]
|
85
|
+
scanner = select_scanner(database)
|
86
|
+
ss = StringScanner.new(query_string)
|
87
|
+
length = attack_string.length
|
88
|
+
while ss.scan_until(regexp)
|
89
|
+
# the pos of StringScanner is at the end of the regexp (input string),
|
90
|
+
# we need the beginning
|
91
|
+
idx = ss.pos - attack_string.length
|
92
|
+
last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
|
93
|
+
next unless last_boundary && boundary
|
94
|
+
|
95
|
+
result ||= build_attack_result(context)
|
96
|
+
|
97
|
+
record_match(idx, length, boundary, last_boundary, kwargs)
|
98
|
+
append_match(context, input_analysis_result, result, query_string, **kwargs)
|
99
|
+
end
|
100
|
+
|
101
|
+
result
|
102
|
+
end
|
103
|
+
|
104
|
+
def select_scanner database
|
105
|
+
@scanners ||= {
|
106
|
+
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
|
107
|
+
Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
|
108
|
+
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
|
109
|
+
Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
|
110
|
+
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
|
111
|
+
Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new,
|
112
|
+
Contrast::Agent::Protect::Policy::AppliesNoSqliRule::DATABASE_NOSQL =>
|
113
|
+
Contrast::Agent::Protect::Rule::NoSqli::MongoNoSqlScanner.new
|
114
|
+
}.cs__freeze
|
115
|
+
|
116
|
+
@default_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
|
117
|
+
@scanners[database.to_s] || @default_scanner
|
118
|
+
end
|
119
|
+
|
120
|
+
def record_match idx, length, boundary, last_boundary, kwargs
|
121
|
+
kwargs[:start_idx] = idx
|
122
|
+
kwargs[:end_idx] = idx + length
|
123
|
+
kwargs[:boundary_overrun_idx] = boundary
|
124
|
+
kwargs[:input_boundary_idx] = last_boundary
|
125
|
+
end
|
126
|
+
|
127
|
+
def append_match context, input_analysis_result, result, query_string, **kwargs
|
128
|
+
input_analysis_result.attack_count = input_analysis_result.attack_count + 1
|
129
|
+
update_successful_attack_response(context, input_analysis_result, result, query_string)
|
130
|
+
append_sample(context, input_analysis_result, result, query_string, **kwargs)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require 'contrast/agent/protect/rule/base_service'
|
5
5
|
require 'contrast/agent/protect/policy/applies_sqli_rule'
|
6
|
+
require 'contrast/agent/protect/rule/sql_sample_builder'
|
6
7
|
|
7
8
|
module Contrast
|
8
9
|
module Agent
|
@@ -10,6 +11,12 @@ module Contrast
|
|
10
11
|
module Rule
|
11
12
|
# The Ruby implementation of the Protect SQL Injection rule.
|
12
13
|
class Sqli < Contrast::Agent::Protect::Rule::BaseService
|
14
|
+
# Generate a sample for the SQLI injection detection rule, allowing for reporting to and rendering
|
15
|
+
# by TeamServer
|
16
|
+
include SqlSampleBuilder::SqliSample
|
17
|
+
# Defining build_attack_with_match method
|
18
|
+
include SqlSampleBuilder::AttackBuilder
|
19
|
+
|
13
20
|
NAME = 'sql-injection'
|
14
21
|
BLOCK_MESSAGE = 'SQLi rule triggered. Response blocked.'
|
15
22
|
|
@@ -31,76 +38,6 @@ module Contrast
|
|
31
38
|
|
32
39
|
raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
|
33
40
|
end
|
34
|
-
|
35
|
-
def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
|
36
|
-
attack_string = input_analysis_result.value
|
37
|
-
regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
|
38
|
-
|
39
|
-
return unless query_string.match?(regexp)
|
40
|
-
|
41
|
-
database = kwargs[:database]
|
42
|
-
scanner = select_scanner(database)
|
43
|
-
|
44
|
-
ss = StringScanner.new(query_string)
|
45
|
-
length = attack_string.length
|
46
|
-
while ss.scan_until(regexp)
|
47
|
-
# the pos of StringScanner is at the end of the regexp (input string),
|
48
|
-
# we need the beginning
|
49
|
-
idx = ss.pos - attack_string.length
|
50
|
-
last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
|
51
|
-
next unless last_boundary && boundary
|
52
|
-
|
53
|
-
result ||= build_attack_result(context)
|
54
|
-
record_match(idx, length, boundary, last_boundary, kwargs)
|
55
|
-
append_match(context, input_analysis_result, result, query_string, **kwargs)
|
56
|
-
end
|
57
|
-
|
58
|
-
result
|
59
|
-
end
|
60
|
-
|
61
|
-
protected
|
62
|
-
|
63
|
-
def build_sample context, input_analysis_result, candidate_string, **kwargs
|
64
|
-
input = input_analysis_result.value
|
65
|
-
|
66
|
-
sample = build_base_sample(context, input_analysis_result)
|
67
|
-
sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
|
68
|
-
sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
|
69
|
-
sample.sqli.start_idx = sample.sqli.query.index(input).to_i
|
70
|
-
sample.sqli.end_idx = sample.sqli.start_idx + input.length
|
71
|
-
sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
|
72
|
-
sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
|
73
|
-
sample
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
def record_match idx, length, boundary, last_boundary, kwargs
|
79
|
-
kwargs[:start_idx] = idx
|
80
|
-
kwargs[:end_idx] = idx + length
|
81
|
-
kwargs[:boundary_overrun_idx] = boundary
|
82
|
-
kwargs[:input_boundary_idx] = last_boundary
|
83
|
-
end
|
84
|
-
|
85
|
-
def append_match context, input_analysis_result, result, query_string, **kwargs
|
86
|
-
input_analysis_result.attack_count = input_analysis_result.attack_count + 1
|
87
|
-
update_successful_attack_response(context, input_analysis_result, result, query_string)
|
88
|
-
append_sample(context, input_analysis_result, result, query_string, **kwargs)
|
89
|
-
end
|
90
|
-
|
91
|
-
def select_scanner database
|
92
|
-
@sql_scanners ||= {
|
93
|
-
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
|
94
|
-
Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
|
95
|
-
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
|
96
|
-
Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
|
97
|
-
Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
|
98
|
-
Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new
|
99
|
-
}.cs__freeze
|
100
|
-
|
101
|
-
@default_sql_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
|
102
|
-
@sql_scanners[database.to_s] || @default_sql_scanner
|
103
|
-
end
|
104
41
|
end
|
105
42
|
end
|
106
43
|
end
|
@@ -9,7 +9,7 @@ module Contrast
|
|
9
9
|
# Because communication between the Agent/Service and TeamServer can only be initiated by outbound connections
|
10
10
|
# from the Agent/Service, we must provide a mechanism for the TeamServer to direct the Agent to take a specific
|
11
11
|
# action. This action is referred to as a Reaction. This class is how we handle those Reaction messages.
|
12
|
-
|
12
|
+
module ReactionProcessor
|
13
13
|
extend Contrast::Components::Logger::InstanceMethods
|
14
14
|
|
15
15
|
# Process the given Reactions from the application settings based on what
|
@@ -92,10 +92,12 @@ module Contrast
|
|
92
92
|
defined?(Rack::Multipart::UploadedFile) &&
|
93
93
|
body.is_a?(Rack::Multipart::UploadedFile)
|
94
94
|
|
95
|
-
logger.trace(
|
95
|
+
logger.trace('not parsing uploaded file body',
|
96
|
+
file_name: body.original_filename,
|
97
|
+
content_type: body.content_type)
|
96
98
|
@_body = nil
|
97
99
|
else
|
98
|
-
logger.trace(
|
100
|
+
logger.trace('parsing body from request', body_type: body.cs__class.cs__name)
|
99
101
|
@_body = Contrast::Utils::StringUtils.force_utf8(read_body(body))
|
100
102
|
end
|
101
103
|
|
@@ -183,8 +185,11 @@ module Contrast
|
|
183
185
|
res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
|
184
186
|
res
|
185
187
|
when Enumerable
|
186
|
-
|
187
|
-
|
188
|
+
idx = 0
|
189
|
+
res = {}
|
190
|
+
while idx < val.length
|
191
|
+
res.merge! normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]")
|
192
|
+
idx += 1
|
188
193
|
end
|
189
194
|
res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
|
190
195
|
res
|
@@ -4,15 +4,14 @@
|
|
4
4
|
require 'contrast/utils/timer'
|
5
5
|
require 'contrast/agent/request'
|
6
6
|
require 'contrast/agent/response'
|
7
|
-
require 'contrast/
|
7
|
+
require 'contrast/agent/inventory/database_config'
|
8
8
|
require 'contrast/components/logger'
|
9
9
|
require 'contrast/components/scope'
|
10
10
|
|
11
11
|
module Contrast
|
12
12
|
module Agent
|
13
|
-
# This class acts to encapsulate information about the currently executed
|
14
|
-
#
|
15
|
-
# in a standardized and normalized format which the Agent understands.
|
13
|
+
# This class acts to encapsulate information about the currently executed request, making it available to the Agent
|
14
|
+
# for the duration of the request in a standardized and normalized format which the Agent understands.
|
16
15
|
#
|
17
16
|
# @attr_reader timer [Contrast::Utils::Timer] when the context was created
|
18
17
|
# @attr_reader logging_hash [Hash] context used to log the request
|
@@ -61,14 +60,10 @@ module Contrast
|
|
61
60
|
# generic holder for properties that can be set throughout this request
|
62
61
|
@_properties = {}
|
63
62
|
|
64
|
-
@sample = true
|
65
|
-
|
66
63
|
if ::Contrast::ASSESS.enabled?
|
67
|
-
@
|
64
|
+
@sample_req, @sample_res = Contrast::Utils::Assess::SamplingUtil.instance.sample?(@request)
|
68
65
|
end
|
69
66
|
|
70
|
-
@sample_response &&= ::Contrast::ASSESS.scan_response?
|
71
|
-
|
72
67
|
append_route_coverage(Contrast::Agent.framework_manager.get_route_dtm(@request))
|
73
68
|
end
|
74
69
|
end
|
@@ -78,16 +73,38 @@ module Contrast
|
|
78
73
|
end
|
79
74
|
|
80
75
|
def analyze_request?
|
81
|
-
|
76
|
+
analyze_request_assess? || analyze_req_res_protect?
|
82
77
|
end
|
83
78
|
|
84
79
|
def analyze_response?
|
85
|
-
|
80
|
+
analyze_response_assess? || analyze_req_res_protect?
|
81
|
+
end
|
82
|
+
|
83
|
+
def analyze_req_res_protect?
|
84
|
+
::Contrast::PROTECT.enabled?
|
86
85
|
end
|
87
86
|
|
88
|
-
|
89
|
-
|
90
|
-
|
87
|
+
def analyze_request_assess?
|
88
|
+
return false unless analyze_req_res_assess?
|
89
|
+
|
90
|
+
@sample_req
|
91
|
+
end
|
92
|
+
|
93
|
+
def analyze_response_assess?
|
94
|
+
return false unless analyze_req_res_assess?
|
95
|
+
|
96
|
+
@sample_res &&= ::Contrast::ASSESS.scan_response?
|
97
|
+
end
|
98
|
+
|
99
|
+
def analyze_req_res_assess?
|
100
|
+
::Contrast::ASSESS.enabled?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Convert the discovered route for this request to appropriate forms and disseminate it to those locations
|
104
|
+
# where it is necessary for our route coverage and finding vulnerability discovery features to function.
|
105
|
+
#
|
106
|
+
# @param route [Contrast::Api::Dtm::RouteCoverage, nil] the route of the current request, as determined from the
|
107
|
+
# framework
|
91
108
|
def append_route_coverage route
|
92
109
|
return unless route
|
93
110
|
|
@@ -108,8 +125,8 @@ module Contrast
|
|
108
125
|
# Collect the results for the given rule with the given action
|
109
126
|
#
|
110
127
|
# @param rule [String] the id of the rule to which the results apply
|
111
|
-
# @param response_type [Symbol] the result of the response, matching a
|
112
|
-
#
|
128
|
+
# @param response_type [Symbol] the result of the response, matching a value of
|
129
|
+
# Contrast::Api::Dtm::AttackResult::ResponseType
|
113
130
|
# @return [Array<Contrast::Api::Dtm::AttackResult>]
|
114
131
|
def results_for rule, response_type = nil
|
115
132
|
if response_type.nil?
|
@@ -130,8 +147,10 @@ module Contrast
|
|
130
147
|
handle_protect_state(service_response)
|
131
148
|
ia = service_response.input_analysis
|
132
149
|
if ia
|
133
|
-
logger.trace
|
134
|
-
|
150
|
+
if logger.trace?
|
151
|
+
logger.trace('Analysis from Contrast Service', evaluations: ia.results.length)
|
152
|
+
logger.trace('Results', input_analysis: ia.inspect)
|
153
|
+
end
|
135
154
|
@speedracer_input_analysis = ia
|
136
155
|
speedracer_input_analysis.request = request
|
137
156
|
else
|
@@ -145,10 +164,9 @@ module Contrast
|
|
145
164
|
false
|
146
165
|
end
|
147
166
|
|
148
|
-
# NOTE: this method is only used as a backstop if Speedracer sends Input Evaluations
|
149
|
-
#
|
150
|
-
#
|
151
|
-
# Speedracer for any attacks detected during prefilter.
|
167
|
+
# NOTE: this method is only used as a backstop if Speedracer sends Input Evaluations when the protect state
|
168
|
+
# indicates a security exception should be thrown. This method ensures that the attack reports are generated.
|
169
|
+
# Normally these should be generated on Speedracer for any attacks detected during prefilter.
|
152
170
|
#
|
153
171
|
# @param agent_settings [Contrast::Api::Settings::AgentSettings]
|
154
172
|
def handle_protect_state agent_settings
|
@@ -165,12 +183,11 @@ module Contrast
|
|
165
183
|
raise Contrast::SecurityException.new(nil, (state.security_message || 'Blocking suspicious behavior'))
|
166
184
|
end
|
167
185
|
|
168
|
-
# append anything we've learned to the request seen message
|
169
|
-
#
|
170
|
-
# been accumulated since the last request
|
186
|
+
# append anything we've learned to the request seen message this is the sum-total of all inventory information
|
187
|
+
# that has been accumulated since the last request
|
171
188
|
def extract_after rack_response
|
172
189
|
@response = Contrast::Agent::Response.new(rack_response)
|
173
|
-
activity.http_response = @response.dtm if @
|
190
|
+
activity.http_response = @response.dtm if @sample_res
|
174
191
|
rescue StandardError => e
|
175
192
|
logger.error('Unable to extract information after request', e)
|
176
193
|
end
|
@@ -185,14 +202,13 @@ module Contrast
|
|
185
202
|
|
186
203
|
def reset_activity
|
187
204
|
@activity = Contrast::Api::Dtm::Activity.new(http_request: request.dtm)
|
188
|
-
@server_activity = Contrast::Api::Dtm::ServerActivity.new
|
205
|
+
@server_activity = Contrast::Api::Dtm::ServerActivity.new
|
189
206
|
@observed_route = Contrast::Api::Dtm::ObservedRoute.new
|
190
207
|
end
|
191
208
|
|
192
209
|
private
|
193
210
|
|
194
|
-
# Generate attack results directly from any evaluations on the
|
195
|
-
# agent settings object.
|
211
|
+
# Generate attack results directly from any evaluations on the agent settings object.
|
196
212
|
#
|
197
213
|
# @param agent_settings [Contrast::Api::Settings::AgentSettings]
|
198
214
|
def build_attack_results agent_settings
|
@@ -204,12 +220,14 @@ module Contrast
|
|
204
220
|
rule = ::Contrast::PROTECT.rule(rule_id)
|
205
221
|
next unless rule
|
206
222
|
|
207
|
-
logger.debug
|
223
|
+
if logger.debug?
|
224
|
+
logger.debug('Building attack result from Contrast Service input analysis result',
|
225
|
+
result: ia_result.inspect)
|
226
|
+
end
|
208
227
|
|
209
228
|
attack_result = if rule.mode == :BLOCK
|
210
|
-
# special case for rules (like reflected xss)
|
211
|
-
#
|
212
|
-
# mode but now are just block at perimeter
|
229
|
+
# special case for rules (like reflected xss) that used to have an infilter / block mode
|
230
|
+
# but now are just block at perimeter
|
213
231
|
rule.build_attack_with_match(self, ia_result, attack_results_by_rule[rule_id],
|
214
232
|
ia_result.value)
|
215
233
|
else
|
@@ -16,8 +16,7 @@ module Contrast
|
|
16
16
|
# terminate requests on attack detection if set to block at perimeter
|
17
17
|
def prefilter
|
18
18
|
context = Contrast::Agent::REQUEST_TRACKER.current
|
19
|
-
|
20
|
-
return unless context&.analyze_request? || ::Contrast::PROTECT.enabled?
|
19
|
+
return unless context&.analyze_request?
|
21
20
|
|
22
21
|
logger.trace_with_time('Running prefilter...') do
|
23
22
|
map { |rule| rule.prefilter(context) }
|
@@ -33,8 +32,7 @@ module Contrast
|
|
33
32
|
# has been created. The main actions here are analyzing the response for unsafe state or actions.
|
34
33
|
def postfilter
|
35
34
|
context = Contrast::Agent::REQUEST_TRACKER.current
|
36
|
-
|
37
|
-
return unless context&.analyze_response? || ::Contrast::PROTECT.enabled?
|
35
|
+
return unless context&.analyze_response?
|
38
36
|
|
39
37
|
logger.trace_with_time('Running postfilter...') do
|
40
38
|
map { |rule| rule.postfilter(context) }
|
data/lib/contrast/agent/scope.rb
CHANGED
@@ -104,39 +104,51 @@ module Contrast
|
|
104
104
|
exit_split_scope!
|
105
105
|
end
|
106
106
|
|
107
|
-
#
|
108
|
-
#
|
109
|
-
# Prefer the static methods if you know what scope you need at the call site.
|
107
|
+
# Static methods to be used, the cases are defined by the usage from the above methods
|
108
|
+
# if more methods are added - please extend the case statements as they are no longed dynamic
|
110
109
|
def in_scope? name
|
111
|
-
|
112
|
-
|
113
|
-
|
110
|
+
case name
|
111
|
+
when :contrast
|
112
|
+
in_contrast_scope?
|
113
|
+
when :deserialization
|
114
|
+
in_deserialization_scope?
|
115
|
+
when :split
|
116
|
+
in_split_scope?
|
117
|
+
else
|
118
|
+
raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
|
119
|
+
end
|
114
120
|
end
|
115
121
|
|
116
122
|
def enter_scope! name
|
117
|
-
|
118
|
-
|
119
|
-
|
123
|
+
case name
|
124
|
+
when :contrast
|
125
|
+
enter_contrast_scope!
|
126
|
+
when :deserialization
|
127
|
+
enter_deserialization_scope!
|
128
|
+
when :split
|
129
|
+
enter_split_scope!
|
130
|
+
else
|
131
|
+
raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
|
132
|
+
end
|
120
133
|
end
|
121
134
|
|
122
135
|
def exit_scope! name
|
123
|
-
|
124
|
-
|
125
|
-
|
136
|
+
case name
|
137
|
+
when :contrast
|
138
|
+
exit_contrast_scope!
|
139
|
+
when :deserialization
|
140
|
+
exit_deserialization_scope!
|
141
|
+
when :split
|
142
|
+
exit_split_scope!
|
143
|
+
else
|
144
|
+
raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
|
145
|
+
end
|
126
146
|
end
|
127
147
|
|
128
148
|
class << self
|
129
149
|
def valid_scope? scope_sym
|
130
150
|
Contrast::Agent::Scope::SCOPE_LIST.include? scope_sym
|
131
151
|
end
|
132
|
-
|
133
|
-
def ensure_valid_scope! scope_sym
|
134
|
-
unless valid_scope? scope_sym # rubocop:disable Style/GuardClause
|
135
|
-
with_contrast_scope do
|
136
|
-
raise NoMethodError, "Scope '#{ scope_sym.inspect }' is not registered as a scope."
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
152
|
end
|
141
153
|
end
|
142
154
|
end
|
@@ -28,7 +28,7 @@ module Contrast
|
|
28
28
|
|
29
29
|
app_update_msg = Contrast::Api::Dtm::ApplicationUpdate.build
|
30
30
|
|
31
|
-
Contrast::
|
31
|
+
Contrast::Agent::Inventory::DatabaseConfig.append_db_config(app_update_msg)
|
32
32
|
Contrast::Agent.messaging_queue.send_event_eventually(app_update_msg)
|
33
33
|
end
|
34
34
|
|
@@ -27,18 +27,31 @@ module Contrast
|
|
27
27
|
|
28
28
|
private
|
29
29
|
|
30
|
+
# Use the TracePoint from the :end event, meaning the completion of a definition of a Class or Module (or
|
31
|
+
# really the completion of that piece of a definition, as determined by an `end` statement since there could be
|
32
|
+
# definitions across multiple files) to carry out actions required on definition. This typically involves
|
33
|
+
# patching and usage analysis
|
34
|
+
#
|
35
|
+
# @param tracepoint_event [TracePoint] the TracePoint from the :end
|
30
36
|
def process tracepoint_event
|
31
37
|
with_contrast_scope do
|
32
|
-
|
33
|
-
|
38
|
+
# the Module or Class that was loaded during this event
|
34
39
|
loaded_module = tracepoint_event.self
|
40
|
+
# the file being loaded that contained this definition
|
35
41
|
path = tracepoint_event.path
|
36
42
|
return if path&.include?('contrast')
|
37
43
|
|
44
|
+
logger.trace('Received TracePoint end event', module: loaded_module, path: path)
|
45
|
+
|
46
|
+
Contrast::Agent.framework_manager.register_late_framework(loaded_module)
|
38
47
|
Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.associate_file(path) if path
|
39
48
|
Contrast::Agent::Patching::Policy::Patcher.patch_specific_module(loaded_module)
|
40
|
-
|
49
|
+
if RUBY_VERSION < '2.6.0' # TODO: RUBY-714 remove guard w/ EOL of 2.5
|
50
|
+
Contrast::Agent::Assess::Policy::RewriterPatch.rewrite_interpolation(loaded_module)
|
51
|
+
end
|
41
52
|
Contrast::Agent::Assess::Policy::PolicyScanner.scan(tracepoint_event)
|
53
|
+
rescue StandardError => e
|
54
|
+
logger.error('Unable to complete TracePoint analysis', e, module: loaded_module, path: path)
|
42
55
|
end
|
43
56
|
end
|
44
57
|
end
|