contrast-agent 7.2.0 → 7.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/contrast/agent/assess/policy/policy_node.rb +25 -6
- data/lib/contrast/agent/assess/policy/propagator/response.rb +64 -0
- data/lib/contrast/agent/assess/policy/propagator.rb +1 -0
- data/lib/contrast/agent/assess/policy/source_method.rb +5 -0
- data/lib/contrast/agent/assess/rule/response/body_rule.rb +22 -7
- data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +4 -1
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
- data/lib/contrast/agent/protect/rule/base.rb +5 -1
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
- data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
- data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
- data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
- data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
- data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
- data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
- data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
- data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
- data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
- data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
- data/lib/contrast/agent/telemetry/base.rb +28 -2
- data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/client.rb +10 -2
- data/lib/contrast/agent/telemetry/exception/obfuscate.rb +4 -3
- data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
- data/lib/contrast/agent/telemetry/identifier.rb +13 -26
- data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
- data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
- data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
- data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
- data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/assess.rb +33 -6
- data/lib/contrast/components/base.rb +4 -2
- data/lib/contrast/components/config.rb +6 -6
- data/lib/contrast/components/protect.rb +11 -1
- data/lib/contrast/components/sampling.rb +15 -10
- data/lib/contrast/config/diagnostics/command_line.rb +2 -2
- data/lib/contrast/config/diagnostics/environment_variables.rb +5 -2
- data/lib/contrast/config/diagnostics/tools.rb +15 -5
- data/lib/contrast/config/yaml_file.rb +8 -0
- data/lib/contrast/configuration.rb +61 -29
- data/lib/contrast/framework/rails/support.rb +3 -0
- data/lib/contrast/logger/application.rb +3 -3
- data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
- data/lib/contrast/utils/assess/propagation_method_utils.rb +2 -0
- data/lib/contrast/utils/metrics_hash.rb +1 -1
- data/lib/contrast/utils/object_share.rb +2 -1
- data/lib/contrast/utils/os.rb +1 -9
- data/lib/contrast/utils/response_utils.rb +12 -0
- data/lib/contrast/utils/timer.rb +2 -0
- data/lib/contrast.rb +9 -2
- data/resources/assess/policy.json +80 -3
- data/ruby-agent.gemspec +1 -1
- metadata +22 -6
- data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -0,0 +1,198 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/utils/lru_cache'
|
5
|
+
require 'contrast/utils/duck_utils'
|
6
|
+
require 'contrast/agent/protect/rule/input_classification/cached_result'
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/statistics'
|
8
|
+
require 'contrast/agent/protect/rule/input_classification/utils'
|
9
|
+
|
10
|
+
module Contrast
|
11
|
+
module Agent
|
12
|
+
module Protect
|
13
|
+
module Rule
|
14
|
+
module InputClassification
|
15
|
+
# This Class with store the input classification results for a given user input.
|
16
|
+
# Among the most used inputs are the headers values for session_id and path values.
|
17
|
+
class LRUCache < Contrast::Utils::LRUCache
|
18
|
+
include Contrast::Components::Logger::InstanceMethods
|
19
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Utils
|
20
|
+
|
21
|
+
# Protect rules will always be fixed number, on other hand the number of inputs will grow,
|
22
|
+
# we need to limit the number of inputs to be cached.
|
23
|
+
RESULTS_CAPACITY = 20
|
24
|
+
|
25
|
+
private :[], :[]=
|
26
|
+
|
27
|
+
# Initialize the LRU Cache.
|
28
|
+
#
|
29
|
+
# @param capacity [Integer] the maximum number of elements to store in the cache. For this
|
30
|
+
# instance it will never reach outside of the number of supported Protect rules.
|
31
|
+
def initialize capacity = 10
|
32
|
+
super(capacity)
|
33
|
+
end
|
34
|
+
|
35
|
+
def mutex
|
36
|
+
@_mutex ||= Mutex.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def with_mutex &block
|
40
|
+
return_type = mutex.synchronize(&block)
|
41
|
+
ensure
|
42
|
+
return_type
|
43
|
+
end
|
44
|
+
|
45
|
+
# Capacity of the statistics will always be the number of rule_id Protect supports.
|
46
|
+
#
|
47
|
+
# @return [Contrast::Agent::Protect::Rule::InputClassification::Statistics]
|
48
|
+
def statistics
|
49
|
+
@_statistics ||= Contrast::Agent::Protect::Rule::InputClassification::Statistics.new
|
50
|
+
end
|
51
|
+
|
52
|
+
# Clear the cache and statistics.
|
53
|
+
def clear
|
54
|
+
with_mutex { @cache.clear }
|
55
|
+
clear_statistics
|
56
|
+
end
|
57
|
+
|
58
|
+
# Clear only the statistics.
|
59
|
+
def clear_statistics
|
60
|
+
with_mutex { statistics.send(:clear) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Check if the cache is empty.
|
64
|
+
#
|
65
|
+
# @return [Boolean]
|
66
|
+
def empty?
|
67
|
+
Contrast::Utils::DuckUtils.empty_duck?(@cache)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check if the input is cached and returns it if so and record the statistics for the required input.
|
71
|
+
#
|
72
|
+
# @param rule_id [String] the Protect rule name.
|
73
|
+
# @param input [String] the user input.
|
74
|
+
# @param input_type [Symbol] Type of the input
|
75
|
+
# @param request [Contrast::Agent::Request] the current request.
|
76
|
+
# @return [Contrast::Agent::Protect::InputClassification::CachedResult, nil]
|
77
|
+
def lookout rule_id, input, input_type, request
|
78
|
+
with_mutex { _loockout(rule_id, input, input_type, request) }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Save the input classification result for a given user input.
|
82
|
+
#
|
83
|
+
# @param rule_id [String] the Protect rule name.
|
84
|
+
# @param result [Contrast::Agent::Reporting::InputAnalysisResult]
|
85
|
+
# @param request [Contrast::Agent::Request] the current request.
|
86
|
+
# @return result [Contrast::Agent::Protect::InputClassification::CachedResult, nil]
|
87
|
+
def save rule_id, result, request
|
88
|
+
with_mutex { _save(rule_id, result, request) }
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Check if the input is cached and returns it if so and record the statistics for the required input.
|
94
|
+
#
|
95
|
+
# @param rule_id [String] the Protect rule name.
|
96
|
+
# @param input [String] the user input.
|
97
|
+
# @param input_type [Symbol] Type of the input
|
98
|
+
# @param request [Contrast::Agent::Request] the current request.
|
99
|
+
# @return [Contrast::Agent::Protect::InputClassification::CachedResult, nil]
|
100
|
+
def _loockout rule_id, input, input_type, request
|
101
|
+
cached = retrieve(rule_id, input, input_type)
|
102
|
+
if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
|
103
|
+
# Telemetry event matched.
|
104
|
+
statistics.match!(rule_id, cached, request)
|
105
|
+
cached
|
106
|
+
else
|
107
|
+
# Telemetry event mismatched.
|
108
|
+
statistics.mismatch!(rule_id, input_type)
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
rescue StandardError => e
|
112
|
+
logger.error("[IA_LRU_Cache] Error while looking for #{ input_type } for #{ rule_id }", error: e)
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
|
116
|
+
# Save the input classification result for a given user input.
|
117
|
+
#
|
118
|
+
# @param rule_id [String] the Protect rule name.
|
119
|
+
# @param result [Contrast::Agent::Reporting::InputAnalysisResult]
|
120
|
+
# @param request [Contrast::Agent::Request] the current request.
|
121
|
+
# @return result [Contrast::Agent::Protect::InputClassification::CachedResult, nil]
|
122
|
+
def _save rule_id, result, request
|
123
|
+
cached_result = Contrast::Agent::Protect::InputClassification::CachedResult.new(result, request.__id__)
|
124
|
+
new_entry = safe_extract(push(rule_id, cached_result)) unless cached_result.empty?
|
125
|
+
statistics.push(rule_id, cached_result)
|
126
|
+
|
127
|
+
new_entry
|
128
|
+
rescue StandardError => e
|
129
|
+
logger.error("[IA_LRU_Cache] Error while saving #{ result } for #{ rule_id }",
|
130
|
+
error: e,
|
131
|
+
stack_trace: e.backtrace)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @param rule_id [String] the Protect rule name.
|
135
|
+
# @param result [Contrast::Agent::Protect::InputClassification::CachedResult, nil]
|
136
|
+
# @return [Array<Contrast::Agent::Protect::InputClassification::CachedResult>, nil]
|
137
|
+
def push rule_id, result
|
138
|
+
return unless result.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
|
139
|
+
return @cache[rule_id] = [result] unless key?(rule_id)
|
140
|
+
|
141
|
+
cached_results = @cache[rule_id]
|
142
|
+
cached_results.shift if cached_results.length >= RESULTS_CAPACITY
|
143
|
+
return @cache[rule_id] << result unless already_saved?(result, rule_id)
|
144
|
+
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns the cached result for a given user input. If the input is not cached, it will return nil.
|
149
|
+
# If the input is cached and key is needed, it will return the cached result and the key and key_type.
|
150
|
+
#
|
151
|
+
# @param rule_id [String] the Protect rule name.
|
152
|
+
# @param input [String] the user input.
|
153
|
+
# @param input_type [Symbol] Type of the input
|
154
|
+
# @return [Contrast::Agent::Protect::InputClassification::CachedResult, nil]
|
155
|
+
def retrieve rule_id, input, input_type
|
156
|
+
# Check to see if cache exist if not just return the keys info.
|
157
|
+
return unless key?(rule_id)
|
158
|
+
|
159
|
+
safe_extract(query_fetch(rule_id, input, input_type))
|
160
|
+
end
|
161
|
+
|
162
|
+
# returns true on first input already saved with the same input.
|
163
|
+
# We will know if the key is needed from the result itself.
|
164
|
+
#
|
165
|
+
# @param cached_result [Contrast::Agent::Protect::InputClassification::CachedResult, nil]
|
166
|
+
# @param rule_id [String]
|
167
|
+
# @return [Boolean]
|
168
|
+
def already_saved? cached_result, rule_id
|
169
|
+
return false unless cached_result.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
|
170
|
+
|
171
|
+
@cache[rule_id].any? do |entry|
|
172
|
+
if entry.result.value == cached_result.result.value &&
|
173
|
+
entry.result.input_type == cached_result.result.input_type
|
174
|
+
|
175
|
+
return true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
false
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns first match for the required input.
|
182
|
+
#
|
183
|
+
# @param rule_id [String] the Protect rule name.
|
184
|
+
# @param input [String] the user input.
|
185
|
+
# @param input_type [Symbol] Type of the input
|
186
|
+
# @return result [Contrast::Agent::Protect::InputClassification::CachedResult, nil]
|
187
|
+
def query_fetch rule_id, input, input_type
|
188
|
+
@cache[rule_id].map do |cached_result|
|
189
|
+
return cached_result if cached_result.result.value == input &&
|
190
|
+
cached_result.result.input_type == input_type
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/agent/protect/rule/input_classification/rates'
|
5
|
+
|
6
|
+
module Contrast
|
7
|
+
module Agent
|
8
|
+
module Protect
|
9
|
+
module Rule
|
10
|
+
module InputClassification
|
11
|
+
# This class will hold match information when input classification is being saved in LRU cache.
|
12
|
+
class MatchRates < Rates
|
13
|
+
# @return [Integer]
|
14
|
+
attr_reader :input_matches
|
15
|
+
# @return [Integer]
|
16
|
+
attr_reader :input_mismatches
|
17
|
+
# @return [Integer]
|
18
|
+
attr_reader :request_matches
|
19
|
+
# @return [String]
|
20
|
+
attr_reader :score_level
|
21
|
+
|
22
|
+
# @param rule_id [String]
|
23
|
+
# @param input_type [Symbol, nil]
|
24
|
+
# @param score_level [String]
|
25
|
+
def initialize rule_id, input_type, score_level
|
26
|
+
super(rule_id, input_type)
|
27
|
+
@input_matches = 0
|
28
|
+
@request_matches = 0
|
29
|
+
@input_mismatches = 0
|
30
|
+
@score_level = score_level if Contrast::Agent::Reporting::ScoreLevel.to_a.include?(score_level)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Increase the match count for the given rule_id and input.
|
34
|
+
def increase_match_for_input
|
35
|
+
@input_matches += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def increase_mismatch_for_input
|
39
|
+
@input_mismatches += 1
|
40
|
+
end
|
41
|
+
|
42
|
+
# increase the missmatch count for the given rule_id and input.
|
43
|
+
def increase_match_for_request
|
44
|
+
@request_matches += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
def empty?
|
48
|
+
super && @input_matches.zero? && @input_mismatches.zero? && @request_matches.zero?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns Agent Telemetry reportable fields.
|
52
|
+
#
|
53
|
+
# @return [Hash]
|
54
|
+
def to_fields
|
55
|
+
{
|
56
|
+
"#{ to_field_title }.input_matches" => input_matches,
|
57
|
+
"#{ to_field_title }.input_mismatches" => input_mismatches,
|
58
|
+
"#{ to_field_title }.request_matches" => request_matches
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/agent/reporting/input_analysis/input_type'
|
5
|
+
require 'contrast/utils/duck_utils'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
module Protect
|
10
|
+
module Rule
|
11
|
+
module InputClassification
|
12
|
+
# This class will hold match information when input classification is being saved in LRU cache.
|
13
|
+
class Rates
|
14
|
+
# Titles:
|
15
|
+
FIELD_NAME = 'ia'
|
16
|
+
TEST_NAME = '_t'
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :rule_id
|
20
|
+
# @return [Symbol, nil]
|
21
|
+
attr_reader :input_type
|
22
|
+
|
23
|
+
def initialize rule_id, input_type
|
24
|
+
@rule_id = rule_id if rule_id
|
25
|
+
@input_type = input_type if Contrast::Agent::Reporting::InputType.to_a.include?(input_type)
|
26
|
+
end
|
27
|
+
|
28
|
+
def empty?
|
29
|
+
Contrast::Utils::DuckUtils.empty_duck?(@input_type)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Override this method to return the required Agent Telemetry fields.
|
33
|
+
# @return [Hash]
|
34
|
+
def to_fields
|
35
|
+
{}
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# @return [String]
|
41
|
+
def to_field_title
|
42
|
+
if (ENV['CONTRAST_AGENT_TELEMETRY_TEST'] = '1')
|
43
|
+
TEST_NAME
|
44
|
+
else
|
45
|
+
FIELD_NAME
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/agent/protect/rule/input_classification/utils'
|
5
|
+
require 'contrast/agent/protect/rule/input_classification/match_rates'
|
6
|
+
require 'contrast/agent/telemetry/input_analysis_cache_event'
|
7
|
+
require 'contrast/agent/reporting/input_analysis/score_level'
|
8
|
+
require 'contrast/agent/reporting/input_analysis/input_type'
|
9
|
+
|
10
|
+
module Contrast
|
11
|
+
module Agent
|
12
|
+
module Protect
|
13
|
+
module Rule
|
14
|
+
module InputClassification
|
15
|
+
# This class will hold match information for each rule when input classification is being saved in LRU cache.
|
16
|
+
class Statistics
|
17
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Utils
|
18
|
+
include Contrast::Components::Logger::InstanceMethods
|
19
|
+
|
20
|
+
attr_reader :data
|
21
|
+
|
22
|
+
# Protect rules will always be fixed number, on other hand the number of inputs will grow,
|
23
|
+
# we need to limit the number of inputs to be cached.
|
24
|
+
CAPACITY = 30
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@data = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# This method will handle the statistics for the input match
|
31
|
+
#
|
32
|
+
# @param rule_id [String] the Protect rule name.
|
33
|
+
# @param cached [Contrast::Agent::Protect::InputClassification::CachedResult]
|
34
|
+
# @param request [Contrast::Agent::Request] the current request.
|
35
|
+
def match! rule_id, cached, request
|
36
|
+
return unless Contrast::Agent::Telemetry::Base.enabled?
|
37
|
+
|
38
|
+
push(rule_id, cached)
|
39
|
+
fetch(rule_id, cached.result.input_type)&.increase_match_for_input
|
40
|
+
return unless cached.request_id == request.__id__
|
41
|
+
|
42
|
+
fetch(rule_id, cached.result.input_type)&.increase_match_for_request
|
43
|
+
end
|
44
|
+
|
45
|
+
# This method will handle the statistics for the input mismatch.
|
46
|
+
# Skip if this is the called with empty cache since it's not fair.
|
47
|
+
#
|
48
|
+
# @param rule_id [String] the Protect rule name.
|
49
|
+
# @param input_type [Symbol] Type of the input
|
50
|
+
def mismatch! rule_id, input_type
|
51
|
+
return unless Contrast::Agent::Telemetry::Base.enabled?
|
52
|
+
return if Contrast::Agent::Protect::InputAnalyzer.lru_cache.empty?
|
53
|
+
|
54
|
+
fetch(rule_id, input_type)&.increase_mismatch_for_input
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Array<Contrast::Agent::Telemetry::InputAnalysisCacheEvent>] the events to be sent.
|
58
|
+
def to_events
|
59
|
+
events = []
|
60
|
+
data.each do |_rule_id, match_rates|
|
61
|
+
match_rates.each do |match_rate|
|
62
|
+
event = Contrast::Agent::Telemetry::InputAnalysisCacheEvent.new(match_rate.rule_id, match_rate)
|
63
|
+
next if event.empty?
|
64
|
+
|
65
|
+
events << event
|
66
|
+
end
|
67
|
+
end
|
68
|
+
events
|
69
|
+
rescue StandardError => e
|
70
|
+
logger.error("[IA_LRU_Cache] Error while creating events: #{ e }", stacktrace: e.backtrace)
|
71
|
+
[]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Creates new statisctics for protect rule.
|
75
|
+
#
|
76
|
+
# @param rule_id [String] the Protect rule name.
|
77
|
+
# @param cached [Contrast::Agent::Protect::InputClassification::CachedResult]
|
78
|
+
def push rule_id, cached
|
79
|
+
new_entry = Contrast::Agent::Protect::Rule::InputClassification::MatchRates.
|
80
|
+
new(rule_id, cached.result.input_type, cached.result.score_level)
|
81
|
+
|
82
|
+
@data[rule_id] = [] if Contrast::Utils::DuckUtils.empty_duck?(@data[rule_id])
|
83
|
+
@data[rule_id].shift if @data[rule_id].length >= CAPACITY
|
84
|
+
@data[rule_id] << new_entry unless saved?(rule_id, cached)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the statistics for the protect rule.
|
88
|
+
#
|
89
|
+
# @param rule_id [String] the Protect rule name.
|
90
|
+
# @param input_type [Symbol] Type of the input
|
91
|
+
def fetch rule_id, input_type
|
92
|
+
safe_extract(@data[rule_id]&.select { |e| e.input_type == input_type })
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Checks if the rate is saved for the input.
|
98
|
+
#
|
99
|
+
# @param rule_id [String] the Protect rule name.
|
100
|
+
# @param new_entry [Contrast::Agent::Protect::InputClassification::CachedResult]
|
101
|
+
def saved? rule_id, new_entry
|
102
|
+
!fetch(rule_id, new_entry&.result&.input_type).nil?
|
103
|
+
end
|
104
|
+
|
105
|
+
# Call this method from the LRU Cache to get the statistics for a given rule_id, so it could be
|
106
|
+
# thread safe.
|
107
|
+
def clear
|
108
|
+
@data.clear
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Contrast
|
5
|
+
module Agent
|
6
|
+
module Protect
|
7
|
+
module Rule
|
8
|
+
module InputClassification
|
9
|
+
# Utils module for Input Classification.
|
10
|
+
module Utils
|
11
|
+
# Extracts data return as array from the cache.
|
12
|
+
#
|
13
|
+
# @param object [NilClass, Array<Contrast::Agent::Protect::InputClassification::CachedResult>]
|
14
|
+
# @return object [Contrast::Agent::Protect::InputClassification::CachedResult]
|
15
|
+
def safe_extract object
|
16
|
+
Array(object)[0]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -4,7 +4,7 @@
|
|
4
4
|
require 'contrast/utils/object_share'
|
5
5
|
require 'contrast/agent/protect/rule/no_sqli/no_sqli'
|
6
6
|
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
7
|
-
require 'contrast/
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
8
8
|
|
9
9
|
module Contrast
|
10
10
|
module Agent
|
@@ -15,7 +15,7 @@ module Contrast
|
|
15
15
|
# to be analyzed at the sink level.
|
16
16
|
module NoSqliInputClassification
|
17
17
|
class << self
|
18
|
-
include
|
18
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
19
19
|
|
20
20
|
NOSQL_COMMENT_REGEXP = %r{"\s*(?:<--|//)}.cs__freeze
|
21
21
|
NOSQL_OR_REGEXP = /(?=(\s+\|\|\s+))/.cs__freeze
|
@@ -96,8 +96,15 @@ module Contrast
|
|
96
96
|
#
|
97
97
|
# @return res [Contrast::Agent::Reporting::InputAnalysisResult]
|
98
98
|
def create_new_input_result request, rule_id, input_type, value
|
99
|
-
|
100
|
-
|
99
|
+
return unless Contrast::AGENT_LIB
|
100
|
+
|
101
|
+
# Cache retrieve
|
102
|
+
cached = Contrast::Agent::Protect::InputAnalyzer.lru_cache.lookout(rule_id, value, input_type, request)
|
103
|
+
return cached.result if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
|
104
|
+
|
105
|
+
eval_value = base64_decode_input(value, input_type)
|
106
|
+
score = evaluate_patterns(eval_value)
|
107
|
+
score = evaluate_rules(eval_value, score)
|
101
108
|
|
102
109
|
score_level = if definite_attack?(score)
|
103
110
|
DEFINITEATTACK
|
@@ -106,9 +113,12 @@ module Contrast
|
|
106
113
|
else
|
107
114
|
IGNORE
|
108
115
|
end
|
109
|
-
|
110
|
-
add_needed_key(request,
|
111
|
-
|
116
|
+
ia_result = new_ia_result(rule_id, input_type, score_level, request.path, value)
|
117
|
+
add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
|
118
|
+
|
119
|
+
# Cache save. Cache must be saved after the input evaluation is completed.
|
120
|
+
Contrast::Agent::Protect::InputAnalyzer.lru_cache.save(rule_id, ia_result, request)
|
121
|
+
ia_result
|
112
122
|
end
|
113
123
|
|
114
124
|
# This method evaluates the patterns relevant to NoSQL Injection to check whether
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Copyright (c) 2023 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/
|
4
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
5
5
|
|
6
6
|
module Contrast
|
7
7
|
module Agent
|
@@ -15,27 +15,31 @@ module Contrast
|
|
15
15
|
|
16
16
|
THRESHOLD = 90.cs__freeze
|
17
17
|
class << self
|
18
|
-
include
|
18
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
#
|
23
|
-
# key if needed and Creates new isntance of InputAnalysisResult.
|
22
|
+
# Creates new instance of AgentLib evaluation result with direct call to AgentLib.
|
24
23
|
#
|
25
|
-
# @param request [Contrast::Agent::Request] the current request context.
|
26
24
|
# @param rule_id [String] The name of the Protect Rule.
|
27
25
|
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
28
26
|
# @param value [String, Array<String>] the value of the input.
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
Contrast::AGENT_LIB.rule_set[rule_id],
|
37
|
-
Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
|
27
|
+
def build_input_eval rule_id, input_type, value
|
28
|
+
Contrast::AGENT_LIB.eval_input(value,
|
29
|
+
Contrast::Agent::Protect::Rule::InputClassification::Base.
|
30
|
+
convert_input_type(input_type),
|
31
|
+
Contrast::AGENT_LIB.rule_set[rule_id],
|
32
|
+
Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
|
33
|
+
end
|
38
34
|
|
35
|
+
# Creates specific result from the AgentLib evaluation.
|
36
|
+
#
|
37
|
+
# @param rule_id [String] The name of the Protect Rule.
|
38
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
39
|
+
# @param value [String, Array<String>] the value of the input.
|
40
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
41
|
+
# @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
|
42
|
+
def build_ia_result rule_id, input_type, value, request, input_eval
|
39
43
|
ia_result = new_ia_result(rule_id, input_type, request.path, value)
|
40
44
|
score = input_eval&.score || 0
|
41
45
|
if score >= THRESHOLD
|
@@ -50,7 +54,6 @@ module Contrast
|
|
50
54
|
else
|
51
55
|
ia_result.score_level = IGNORE
|
52
56
|
end
|
53
|
-
add_needed_key(request, ia_result, input_type, value)
|
54
57
|
ia_result
|
55
58
|
end
|
56
59
|
end
|
@@ -5,7 +5,7 @@ require 'contrast/utils/object_share'
|
|
5
5
|
require 'contrast/agent/reporting/input_analysis/input_type'
|
6
6
|
require 'contrast/agent/reporting/input_analysis/score_level'
|
7
7
|
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
8
|
-
require 'contrast/
|
8
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
9
9
|
|
10
10
|
module Contrast
|
11
11
|
module Agent
|
@@ -17,7 +17,7 @@ module Contrast
|
|
17
17
|
module SqliInputClassification
|
18
18
|
WORTHWATCHING_MATCH = 'sqli-worth-watching-v2'.cs__freeze
|
19
19
|
class << self
|
20
|
-
include
|
20
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
21
21
|
include Contrast::Components::Logger::InstanceMethods
|
22
22
|
end
|
23
23
|
end
|
data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
require 'contrast/utils/object_share'
|
5
5
|
require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload'
|
6
6
|
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
7
|
-
require 'contrast/
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/base'
|
8
8
|
|
9
9
|
module Contrast
|
10
10
|
module Agent
|
@@ -16,26 +16,30 @@ module Contrast
|
|
16
16
|
UNSAFE_UPLOAD_MATCH = 'unsafe-file-upload-input-tracing-v1'
|
17
17
|
|
18
18
|
class << self
|
19
|
-
include
|
19
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
-
#
|
24
|
-
# key if needed and Creates new isntance of InputAnalysisResult.
|
23
|
+
# Creates new instance of AgentLib evaluation result with direct call to AgentLib.
|
25
24
|
#
|
26
|
-
# @param request [Contrast::Agent::Request] the current request context.
|
27
25
|
# @param rule_id [String] The name of the Protect Rule.
|
28
|
-
# @param
|
26
|
+
# @param _input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
29
27
|
# @param value [String, Array<String>] the value of the input.
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
Contrast::AGENT_LIB.rule_set[rule_id],
|
37
|
-
Contrast::AGENT_LIB.eval_option[:NONE]))
|
28
|
+
def build_input_eval rule_id, _input_type, value
|
29
|
+
Contrast::AGENT_LIB.eval_input(value,
|
30
|
+
Contrast::AGENT_LIB.input_set[:MULTIPART_NAME],
|
31
|
+
Contrast::AGENT_LIB.rule_set[rule_id],
|
32
|
+
Contrast::AGENT_LIB.eval_option[:NONE])
|
33
|
+
end
|
38
34
|
|
35
|
+
# Creates specific result from the AgentLib evaluation.
|
36
|
+
#
|
37
|
+
# @param rule_id [String] The name of the Protect Rule.
|
38
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
39
|
+
# @param value [String, Array<String>] the value of the input.
|
40
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
41
|
+
# @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
|
42
|
+
def build_ia_result rule_id, input_type, value, request, input_eval
|
39
43
|
ia_result = new_ia_result(rule_id, input_type, request.path, value)
|
40
44
|
if input_eval.score >= THRESHOLD
|
41
45
|
ia_result.score_level = DEFINITEATTACK
|
@@ -51,7 +55,6 @@ module Contrast
|
|
51
55
|
else
|
52
56
|
Contrast::Utils::ObjectShare::EMPTY_STRING
|
53
57
|
end
|
54
|
-
|
55
58
|
ia_result
|
56
59
|
end
|
57
60
|
end
|