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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent/assess/policy/policy_node.rb +25 -6
  3. data/lib/contrast/agent/assess/policy/propagator/response.rb +64 -0
  4. data/lib/contrast/agent/assess/policy/propagator.rb +1 -0
  5. data/lib/contrast/agent/assess/policy/source_method.rb +5 -0
  6. data/lib/contrast/agent/assess/rule/response/body_rule.rb +22 -7
  7. data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +4 -1
  8. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  9. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  10. data/lib/contrast/agent/protect/rule/base.rb +5 -1
  11. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
  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/input_classification/base.rb +191 -0
  15. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  19. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  20. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  21. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  22. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  23. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  24. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  25. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  26. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  27. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  28. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  29. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  30. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  31. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  32. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
  33. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  34. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  35. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  36. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  37. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
  38. data/lib/contrast/agent/telemetry/base.rb +28 -2
  39. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  40. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  41. data/lib/contrast/agent/telemetry/client.rb +10 -2
  42. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +4 -3
  43. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  44. data/lib/contrast/agent/telemetry/identifier.rb +13 -26
  45. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  46. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  47. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  48. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  49. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  50. data/lib/contrast/agent/version.rb +1 -1
  51. data/lib/contrast/components/assess.rb +33 -6
  52. data/lib/contrast/components/base.rb +4 -2
  53. data/lib/contrast/components/config.rb +6 -6
  54. data/lib/contrast/components/protect.rb +11 -1
  55. data/lib/contrast/components/sampling.rb +15 -10
  56. data/lib/contrast/config/diagnostics/command_line.rb +2 -2
  57. data/lib/contrast/config/diagnostics/environment_variables.rb +5 -2
  58. data/lib/contrast/config/diagnostics/tools.rb +15 -5
  59. data/lib/contrast/config/yaml_file.rb +8 -0
  60. data/lib/contrast/configuration.rb +61 -29
  61. data/lib/contrast/framework/rails/support.rb +3 -0
  62. data/lib/contrast/logger/application.rb +3 -3
  63. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  64. data/lib/contrast/utils/assess/propagation_method_utils.rb +2 -0
  65. data/lib/contrast/utils/metrics_hash.rb +1 -1
  66. data/lib/contrast/utils/object_share.rb +2 -1
  67. data/lib/contrast/utils/os.rb +1 -9
  68. data/lib/contrast/utils/response_utils.rb +12 -0
  69. data/lib/contrast/utils/timer.rb +2 -0
  70. data/lib/contrast.rb +9 -2
  71. data/resources/assess/policy.json +80 -3
  72. data/ruby-agent.gemspec +1 -1
  73. metadata +22 -6
  74. data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -0,0 +1,191 @@
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/object_share'
5
+ require 'contrast/agent/protect/input_analyzer/input_analyzer'
6
+ require 'contrast/agent/protect/rule/input_classification/extendable'
7
+ require 'contrast/agent/protect/rule/input_classification/encoding'
8
+ require 'contrast/components/logger'
9
+
10
+ module Contrast
11
+ module Agent
12
+ module Protect
13
+ module Rule
14
+ module InputClassification
15
+ # This module will include all the similar information for all input classifications
16
+ # between different rules
17
+ module Base
18
+ UNKNOWN_KEY = 'unknown'
19
+ include Contrast::Components::Logger::InstanceMethods
20
+ include Contrast::Agent::Protect::Rule::InputClassification::Extendable
21
+ include Contrast::Agent::Protect::Rule::InputClassification::Encoding
22
+
23
+ KEYS_NEEDED = [
24
+ COOKIE_VALUE, PARAMETER_VALUE, HEADER, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE
25
+ ].cs__freeze
26
+
27
+ BASE64_INPUT_TYPES = [BODY, COOKIE_VALUE, HEADER, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
28
+
29
+ class << self
30
+ include Contrast::Components::Logger::InstanceMethods
31
+ include Contrast::Agent::Reporting::InputType
32
+
33
+ # Finds key value and type based on input type and value.
34
+ # @param request [Contrast::Agent::Request] the current request context.
35
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
36
+ # @param value [String, Array<String>] the value of the input.
37
+ # @return [Array<(String, Contrast::Agent::Reporting::InputType)>] key and key type.
38
+ def find_key request, input_type, value
39
+ # TODO: RUBY-99999 Add handling for multipart, json and if any missing types.
40
+ case input_type
41
+ when COOKIE_VALUE
42
+ [request.cookies.key(value), Contrast::Agent::Reporting::InputType::COOKIE_NAME]
43
+ when PARAMETER_VALUE, URL_PARAMETER
44
+ [request.parameters.key(value), Contrast::Agent::Reporting::InputType::PARAMETER_NAME]
45
+ when HEADER
46
+ [request.headers.key(value), Contrast::Agent::Reporting::InputType::HEADER]
47
+ when UNKNOWN
48
+ [UNKNOWN_KEY, Contrast::Agent::Reporting::InputType::UNKNOWN]
49
+ else
50
+ [nil, nil]
51
+ end
52
+ rescue StandardError => e
53
+ logger.warn('[InputAnalyzer] Could not find proper key for input traced value', message: e)
54
+ [nil, nil]
55
+ end
56
+
57
+ # Some input types are not yet supported from the AgentLib.
58
+ # This will convert the type to the closet possible if viable,
59
+ # so that the input tracing could be done.
60
+ #
61
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
62
+ # @return [Integer<Contrast::AgentLib::Interface::INPUT_SET>]
63
+ def convert_input_type input_type
64
+ case input_type
65
+ when URI, URL_PARAMETER
66
+ Contrast::AGENT_LIB.input_set[:URI_PATH]
67
+ when BODY, DWR_VALUE, SOCKET, UNDEFINED_TYPE, UNKNOWN, REQUEST, QUERYSTRING
68
+ Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
69
+ when HEADER
70
+ Contrast::AGENT_LIB.input_set[:HEADER_VALUE]
71
+ when MULTIPART_VALUE, MULTIPART_FIELD_NAME
72
+ Contrast::AGENT_LIB.input_set[:MULTIPART_NAME]
73
+ when JSON_ARRAYED_VALUE
74
+ Contrast::AGENT_LIB.input_set[:JSON_KEY]
75
+ when PARAMETER_NAME
76
+ Contrast::AGENT_LIB.input_set[:PARAMETER_KEY]
77
+ else
78
+ Contrast::AGENT_LIB.input_set[input_type]
79
+ end
80
+ rescue StandardError => e
81
+ logger.debug('[InputAnalyzer] Protect Input classification could not determine input type,
82
+ falling back to default',
83
+ error: e)
84
+ Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
85
+ end
86
+ end
87
+
88
+ # Input Classification stage is done to determine if an user input is
89
+ # DEFINITEATTACK or to be ignored.
90
+ #
91
+ # @param rule_id [String] Name of the protect rule.
92
+ # @param input_type [Symbol, Contrast::Agent::Reporting::InputType] The type of the user input.
93
+ # @param value [String, Array<String>] the value of the input.
94
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
95
+ # agent analysis from the current
96
+ # Request.
97
+ # @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
98
+ def classify rule_id, input_type, value, input_analysis
99
+ return unless (rule = Contrast::PROTECT.rule(rule_id))
100
+ return unless rule.applicable_user_inputs.include?(input_type)
101
+ return unless input_analysis.request
102
+
103
+ Array(value).each do |val|
104
+ Array(val).each do |v|
105
+ next unless v
106
+
107
+ result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, v)
108
+ append_result(input_analysis, result)
109
+ end
110
+ end
111
+
112
+ input_analysis
113
+ rescue StandardError => e
114
+ logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
115
+ nil
116
+ end
117
+
118
+ # This methods checks if input is value that matches a key in the input.
119
+ #
120
+ # @param request [Contrast::Agent::Request] the current request context.
121
+ # @param ia_result [Contrast::Agent::Reporting::InputAnalysisResult] result to be updated.
122
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
123
+ # @param value [String, Array<String>] the value of the input.
124
+ #
125
+ # @return result [Array<String, Symbol>] updated with key result.
126
+ def add_needed_key request, ia_result, input_type, value
127
+ ia_result.key, ia_result.key_type = Contrast::Agent::Protect::Rule::InputClassification::Base.
128
+ find_key(request, input_type, value)
129
+ end
130
+
131
+ private
132
+
133
+ # Appends result to the InputAnalysis.
134
+ #
135
+ # @param ia_analysis [Contrast::Agent::Reporting::InputAnalysis] the current input analysis.
136
+ # @param result [Contrast::Agent::Reporting::InputAnalysisResult] result to be appended.
137
+ # @return [Contrast::Agent::Reporting::InputAnalysis] the input analysis with the appended result.
138
+ def append_result ia_analysis, result
139
+ ia_analysis.results << result if result
140
+ ia_analysis
141
+ end
142
+
143
+ # Do not override this method, it will hold base operations, instead overwrite methods called inside
144
+ # of this method.
145
+ # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
146
+ # key if needed and Creates new instance of InputAnalysisResult.
147
+ #
148
+ # @param request [Contrast::Agent::Request] the current request context.
149
+ # @param rule_id [String] The name of the Protect Rule.
150
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
151
+ # @param value [String, Array<String>] the value of the input.
152
+ #
153
+ # @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil]
154
+ def create_new_input_result request, rule_id, input_type, value
155
+ return unless Contrast::AGENT_LIB
156
+
157
+ # Cache retrieve
158
+ cached = Contrast::Agent::Protect::InputAnalyzer.lru_cache.lookout(rule_id, value, input_type, request)
159
+ return cached.result if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
160
+
161
+ # Input evaluation
162
+ input_eval = build_input_eval(rule_id, input_type, base64_decode_input(value, input_type))
163
+ ia_result = build_ia_result(rule_id, input_type, value, request, input_eval)
164
+ return unless ia_result
165
+
166
+ add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
167
+ # Input evaluation end
168
+
169
+ # Cache save. Cache must be saved after the input evaluation is completed.
170
+ Contrast::Agent::Protect::InputAnalyzer.lru_cache.save(rule_id, ia_result, request)
171
+ ia_result
172
+ end
173
+
174
+ # Decodes the value for the given input type.
175
+ # Applies to BODY, COOKIE_VALUE, HEADER, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE
176
+ #
177
+ # @param value [String]
178
+ # @param input_type [Symbol]
179
+ # @return input [String]
180
+ def base64_decode_input value, input_type
181
+ return value unless Contrast::PROTECT.normalize_base64?
182
+ return value unless BASE64_INPUT_TYPES.include?(input_type)
183
+
184
+ cs__decode64(value, input_type)
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,71 @@
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/encoding_rates'
5
+ require 'contrast/agent/telemetry/input_analysis_encoding_event'
6
+ require 'contrast/components/logger'
7
+
8
+ module Contrast
9
+ module Agent
10
+ module Protect
11
+ module Rule
12
+ module InputClassification
13
+ # This class will safe all the information for the Base64 decoding matches per input type.
14
+ class Base64Statistic
15
+ include Contrast::Components::Logger::InstanceMethods
16
+
17
+ # @return [Hash<Contrast::Agent::Protect::Rule::InputClassification::EncodingRates>]
18
+ attr_reader :data
19
+
20
+ # Capacity for request context life cycle.
21
+ CAPACITY = 1000
22
+
23
+ def initialize
24
+ @data = {}
25
+ end
26
+
27
+ # Add a match for the given input type.
28
+ #
29
+ # @param input_type [Symbol]
30
+ def match! input_type
31
+ return @data[input_type]&.increase_match_base64 if @data[input_type]
32
+
33
+ @data[input_type] = Contrast::Agent::Protect::Rule::InputClassification::EncodingRates.new(input_type)
34
+ @data[input_type].increase_match_base64
35
+ end
36
+
37
+ # Add a mismatch for the given input type.
38
+ #
39
+ # @param input_type [Symbol]
40
+ def mismatch! input_type
41
+ return @data[input_type]&.increase_mismatch_base64 if @data[input_type]
42
+
43
+ @data[input_type] = Contrast::Agent::Protect::Rule::InputClassification::EncodingRates.new(input_type)
44
+ @data[input_type].increase_mismatch_base64
45
+ end
46
+
47
+ # Clears statistic data.
48
+ def clear
49
+ @data.clear
50
+ end
51
+
52
+ # @return [Array<Contrast::Agent::Telemetry::InputAnalysisCacheEvent>] the events to be sent.
53
+ def to_events
54
+ events = []
55
+ data.each do |_input_type, encoding_rate|
56
+ event = Contrast::Agent::Telemetry::InputAnalysisEncodingEvent.new(nil, encoding_rate)
57
+ next if event.empty?
58
+
59
+ events << event
60
+ end
61
+ events
62
+ rescue StandardError => e
63
+ logger.error("[Telemetry] Error while creating events: #{ e }", stacktrace: e.backtrace)
64
+ []
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -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