contrast-agent 7.1.0 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/ext/extconf_common.rb +88 -14
  3. data/lib/contrast/agent/assess/policy/source_method.rb +13 -4
  4. data/lib/contrast/agent/assess/policy/trigger_method.rb +12 -18
  5. data/lib/contrast/agent/excluder/excluder.rb +64 -31
  6. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  7. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  8. data/lib/contrast/agent/protect/rule/base.rb +9 -7
  9. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +1 -1
  10. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +29 -13
  11. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
  12. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
  13. data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
  14. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +2 -2
  15. data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  19. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  20. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  21. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  22. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  23. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  24. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  25. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  26. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  27. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  28. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +1 -1
  29. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  30. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +1 -1
  31. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  32. data/lib/contrast/agent/protect/rule/utils/filters.rb +6 -6
  33. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  34. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +1 -1
  35. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  36. data/lib/contrast/agent/reporting/client/interface.rb +132 -0
  37. data/lib/contrast/agent/reporting/client/interface_base.rb +27 -0
  38. data/lib/contrast/agent/reporting/connection_status.rb +0 -1
  39. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  40. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +17 -4
  41. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  42. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  43. data/lib/contrast/agent/reporting/reporter.rb +11 -26
  44. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  45. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  46. data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +1 -1
  47. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
  48. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +47 -6
  49. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +41 -32
  50. data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +144 -0
  51. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +35 -13
  52. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_mode.rb +14 -1
  53. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +11 -11
  54. data/lib/contrast/agent/request/request.rb +27 -12
  55. data/lib/contrast/agent/telemetry/base.rb +44 -19
  56. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  57. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  58. data/lib/contrast/agent/telemetry/client.rb +10 -2
  59. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +97 -0
  60. data/lib/contrast/agent/telemetry/exception.rb +1 -0
  61. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  62. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  63. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  64. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  65. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  66. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  67. data/lib/contrast/agent/version.rb +1 -1
  68. data/lib/contrast/components/config/sources.rb +6 -5
  69. data/lib/contrast/components/config.rb +4 -4
  70. data/lib/contrast/components/protect.rb +11 -1
  71. data/lib/contrast/components/sampling.rb +15 -10
  72. data/lib/contrast/components/settings.rb +9 -0
  73. data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
  74. data/lib/contrast/config/diagnostics/source_config_value.rb +5 -1
  75. data/lib/contrast/config/diagnostics/tools.rb +4 -4
  76. data/lib/contrast/config/validate.rb +2 -2
  77. data/lib/contrast/config/yaml_file.rb +8 -0
  78. data/lib/contrast/configuration.rb +11 -19
  79. data/lib/contrast/framework/grape/support.rb +1 -2
  80. data/lib/contrast/framework/manager.rb +17 -8
  81. data/lib/contrast/framework/rack/support.rb +99 -1
  82. data/lib/contrast/framework/rails/support.rb +4 -2
  83. data/lib/contrast/framework/sinatra/support.rb +1 -2
  84. data/lib/contrast/logger/aliased_logging.rb +18 -9
  85. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  86. data/lib/contrast/utils/hash_utils.rb +21 -2
  87. data/lib/contrast/utils/metrics_hash.rb +1 -1
  88. data/lib/contrast/utils/object_share.rb +2 -1
  89. data/lib/contrast/utils/request_utils.rb +14 -0
  90. data/lib/contrast/utils/response_utils.rb +12 -0
  91. data/lib/contrast/utils/timer.rb +2 -0
  92. data/lib/contrast.rb +9 -2
  93. data/resources/assess/policy.json +11 -0
  94. data/ruby-agent.gemspec +1 -1
  95. metadata +25 -7
  96. data/lib/contrast/agent/reporting/input_analysis/details/bot_blocker_details.rb +0 -27
  97. data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -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
@@ -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