contrast-agent 7.2.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
- data/lib/contrast/agent/protect/rule/base.rb +5 -1
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
- data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
- data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
- data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
- data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
- data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
- data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
- data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
- data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
- data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
- data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
- data/lib/contrast/agent/telemetry/base.rb +28 -2
- data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/client.rb +10 -2
- data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
- data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
- data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
- data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
- data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
- data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/config.rb +4 -4
- data/lib/contrast/components/protect.rb +11 -1
- data/lib/contrast/components/sampling.rb +15 -10
- data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
- data/lib/contrast/config/yaml_file.rb +8 -0
- data/lib/contrast/framework/rails/support.rb +3 -0
- data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
- data/lib/contrast/utils/metrics_hash.rb +1 -1
- data/lib/contrast/utils/object_share.rb +2 -1
- data/lib/contrast/utils/response_utils.rb +12 -0
- data/lib/contrast/utils/timer.rb +2 -0
- data/lib/contrast.rb +9 -2
- data/ruby-agent.gemspec +1 -1
- metadata +21 -6
- data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -0,0 +1,37 @@
|
|
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/duck_utils'
|
5
|
+
require 'contrast/agent/reporting/input_analysis/input_analysis_result'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
module Protect
|
10
|
+
module InputClassification
|
11
|
+
# This Class with store the input classification results for a given user input.
|
12
|
+
class CachedResult
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :result
|
15
|
+
# @return [Integer]
|
16
|
+
attr_reader :request_id
|
17
|
+
|
18
|
+
# Initialize Input Classification Cached Result
|
19
|
+
#
|
20
|
+
# @param result [Contrast::Agent::Reporting::InputAnalysisResult]
|
21
|
+
# @param request_id [Integer] the id of current request.
|
22
|
+
def initialize result, request_id
|
23
|
+
@result = result.dup if result&.cs__is_a?(Contrast::Agent::Reporting::InputAnalysisResult)
|
24
|
+
@request_id = request_id
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check if the input classification result is empty.
|
28
|
+
#
|
29
|
+
# @return [Boolean]
|
30
|
+
def empty?
|
31
|
+
Contrast::Utils::DuckUtils.empty_duck?(@result) && Contrast::Utils::DuckUtils.empty_duck?(@request_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,109 @@
|
|
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 'base64'
|
5
|
+
require 'cgi'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module Contrast
|
9
|
+
module Agent
|
10
|
+
module Protect
|
11
|
+
module Rule
|
12
|
+
module InputClassification
|
13
|
+
# Module to hold different encoding utils.
|
14
|
+
module Encoding
|
15
|
+
include Contrast::Components::Logger::InstanceMethods
|
16
|
+
|
17
|
+
# Still a list is needed for this one, as it is not possible to determine if the value is encoded or not.
|
18
|
+
# As long as the list is short the method has a good percentage of success.
|
19
|
+
KNOWN_DECODING_EXCEPTIONS = %w[cmd].cs__freeze
|
20
|
+
|
21
|
+
# This methods is not performant, but is more safe for false positive.
|
22
|
+
# Base64 check is no trivial task. For example if one passes a value like 'stringdw' it will return true,
|
23
|
+
# or value 'pass', but it is indeed not encoded. using regexp like:
|
24
|
+
#
|
25
|
+
# ^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$
|
26
|
+
#
|
27
|
+
# This will fail with any inputs from above, and this is because any characters with 4 bytes will be
|
28
|
+
# considered as base64 encoded, and without additional context it is impossible to determine if the
|
29
|
+
# value is encoded or not. Not to mention the above regexp will not detect empty spaces.
|
30
|
+
#
|
31
|
+
# Alternative to the above regexp, acting the same way, could be this:
|
32
|
+
#
|
33
|
+
# Base64.strict_encode64(Base64.decode64(value)) == value
|
34
|
+
#
|
35
|
+
# Making an exception list is not a good idea, because it will be hard to maintain.
|
36
|
+
#
|
37
|
+
# The Base64 method will return printable ascii characters, so we can use this to determine if the value is
|
38
|
+
# encoded or not.
|
39
|
+
#
|
40
|
+
# The solution in this case is encodind the value, and then decoding it. If the value is already encoded
|
41
|
+
# it will not be eq to the original value. If the value is not encoded, it will be eq to the original value.
|
42
|
+
#
|
43
|
+
# @param value [String] input to check for encoding status.
|
44
|
+
# @param input_type [Symbol] input type.
|
45
|
+
# @return [Boolean] true if value is base64 encoded, false otherwise.
|
46
|
+
def cs__base64? value, input_type
|
47
|
+
return false unless value.is_a?(String)
|
48
|
+
return false if Contrast::Utils::DuckUtils.empty_duck?(value)
|
49
|
+
|
50
|
+
# Encoded string levels of decoding example:
|
51
|
+
#
|
52
|
+
# Value encoded 'pass' => 'cGFzcw=='
|
53
|
+
# decode level 0 => 'pass'
|
54
|
+
# decode level 1 => '\xA5\xAB,'
|
55
|
+
# decode level 2 => ''
|
56
|
+
check_value = value.dup
|
57
|
+
return false if KNOWN_DECODING_EXCEPTIONS.include?(check_value)
|
58
|
+
|
59
|
+
level = 0
|
60
|
+
iteration = 0
|
61
|
+
until Contrast::Utils::DuckUtils.empty_duck?(Base64.decode64(check_value))
|
62
|
+
iteration += 1
|
63
|
+
# handle cases like 'command' or 'injection' which will check out as encoded regarding the level of
|
64
|
+
# decoding, but will produce ascii escape characters on first iteration, rather than decoded value.
|
65
|
+
level += 1 unless iteration == 2 && ::CGI.escape(check_value) != check_value
|
66
|
+
|
67
|
+
check_value = Base64.decode64(check_value)
|
68
|
+
end
|
69
|
+
|
70
|
+
# if we have more than 2 levels the value is encoded.
|
71
|
+
base64 = level > 1
|
72
|
+
|
73
|
+
# Call base64 statistics:
|
74
|
+
if base64
|
75
|
+
Contrast::Agent::Protect::InputAnalyzer.base64_statistic.match!(input_type)
|
76
|
+
else
|
77
|
+
Contrast::Agent::Protect::InputAnalyzer.base64_statistic.mismatch!(input_type)
|
78
|
+
end
|
79
|
+
|
80
|
+
base64
|
81
|
+
rescue StandardError => e
|
82
|
+
logger.error('Error while checking for base64 encoding',
|
83
|
+
error: e,
|
84
|
+
message: e.message,
|
85
|
+
backtrace: e.backtrace)
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
# This method will decode the value using Base64.decode64, only if value was encoded.
|
90
|
+
# If value is not encoded, it will return the original value.
|
91
|
+
#
|
92
|
+
# @param value [String] input to decode.
|
93
|
+
# @param input_type [Symbol] input type.
|
94
|
+
# @return [String] decoded or original value.
|
95
|
+
# @raise [StandardError]
|
96
|
+
def cs__decode64 value, input_type
|
97
|
+
return value unless cs__base64?(value, input_type)
|
98
|
+
|
99
|
+
Base64.decode64(value)
|
100
|
+
rescue StandardError => e
|
101
|
+
logger.error('Error while decoding base64', error: e, message: e.message, backtrace: e.backtrace)
|
102
|
+
value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,47 @@
|
|
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 EncodingRates < Contrast::Agent::Protect::Rule::InputClassification::Rates
|
13
|
+
# @return [Integer]
|
14
|
+
attr_reader :base64_matches
|
15
|
+
# @return [Integer]
|
16
|
+
attr_reader :base64_mismatches
|
17
|
+
|
18
|
+
def initialize input_type
|
19
|
+
super(nil, input_type)
|
20
|
+
@base64_matches = 0
|
21
|
+
@base64_mismatches = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
# Increase the match count for the given rule_id and input.
|
25
|
+
def increase_match_base64
|
26
|
+
@base64_matches += 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def increase_mismatch_base64
|
30
|
+
@base64_mismatches += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns Agent Telemetry reportable fields.
|
34
|
+
#
|
35
|
+
# @return [Hash]
|
36
|
+
def to_fields
|
37
|
+
{
|
38
|
+
"#{ to_field_title }.input_base64_matches" => base64_matches,
|
39
|
+
"#{ to_field_title }.input_base64_mismatches" => base64_mismatches
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,80 @@
|
|
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/agent/reporting/input_analysis/score_level'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
module Protect
|
10
|
+
module Rule
|
11
|
+
module InputClassification
|
12
|
+
# Module holding the overwritable methods for input classification. This is used by the
|
13
|
+
# Protect rules to define their own input classification logic. To be Used input_types,
|
14
|
+
# score_level, AgentLib, and InputAnalysisResult must be required.
|
15
|
+
module Extendable
|
16
|
+
THRESHOLD = 90.cs__freeze
|
17
|
+
WORTHWATCHING_THRESHOLD = 10.cs__freeze
|
18
|
+
include Contrast::Agent::Reporting::InputType
|
19
|
+
include Contrast::Agent::Reporting::ScoreLevel
|
20
|
+
|
21
|
+
################################################################
|
22
|
+
# Methods to be overwritten for each individual Protect rule. #
|
23
|
+
##############################################################
|
24
|
+
|
25
|
+
# Creates new instance of AgentLib evaluation result with direct call to AgentLib.
|
26
|
+
#
|
27
|
+
# @param rule_id [String] The name of the Protect Rule.
|
28
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
29
|
+
# @param value [String] the value of the input.
|
30
|
+
# @return [Contrast::AgentLib::EvalResult, nil] the result of the input evaluation.
|
31
|
+
def build_input_eval rule_id, input_type, value
|
32
|
+
Contrast::AGENT_LIB.eval_input(value,
|
33
|
+
Contrast::Agent::Protect::Rule::InputClassification::Base.
|
34
|
+
convert_input_type(input_type),
|
35
|
+
Contrast::AGENT_LIB.rule_set[rule_id],
|
36
|
+
Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
|
37
|
+
end
|
38
|
+
|
39
|
+
# Creates specific result from the AgentLib evaluation.
|
40
|
+
#
|
41
|
+
# @param rule_id [String] The name of the Protect Rule.
|
42
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
43
|
+
# @param value [String the value of the input.
|
44
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
45
|
+
# @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
|
46
|
+
# @return [Contrast::Agent::Reporting::InputAnalysisResult, nil] the result of the input analysis.
|
47
|
+
def build_ia_result rule_id, input_type, value, request, input_eval
|
48
|
+
ia_result = new_ia_result(rule_id, input_type, request.path, value)
|
49
|
+
score = input_eval&.score || 0
|
50
|
+
if score >= WORTHWATCHING_THRESHOLD
|
51
|
+
ia_result.score_level = WORTHWATCHING
|
52
|
+
ia_result.ids << self::WORTHWATCHING_MATCH
|
53
|
+
else
|
54
|
+
ia_result.score_level = IGNORE
|
55
|
+
end
|
56
|
+
ia_result
|
57
|
+
end
|
58
|
+
|
59
|
+
# Creates new isntance of InputAnalysisResult with basic info.
|
60
|
+
#
|
61
|
+
# @param rule_id [String] The name of the Protect Rule.
|
62
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
63
|
+
# @param value [String, Array<String>] the value of the input.
|
64
|
+
# @param path [String] the path of the current request context.
|
65
|
+
#
|
66
|
+
# @return res [Contrast::Agent::Reporting::InputAnalysisResult]
|
67
|
+
def new_ia_result rule_id, input_type, path, value = nil
|
68
|
+
res = Contrast::Agent::Reporting::InputAnalysisResult.new
|
69
|
+
res.rule_id = rule_id
|
70
|
+
res.input_type = input_type
|
71
|
+
res.path = path
|
72
|
+
res.value = value
|
73
|
+
res
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -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
|