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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  3. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  4. data/lib/contrast/agent/protect/rule/base.rb +5 -1
  5. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
  6. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
  7. data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
  8. data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
  9. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  10. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  11. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  12. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  13. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  14. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  15. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  19. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  20. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  21. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  22. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  23. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  24. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  25. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  26. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
  27. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  28. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  29. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  30. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  31. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
  32. data/lib/contrast/agent/telemetry/base.rb +28 -2
  33. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  34. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  35. data/lib/contrast/agent/telemetry/client.rb +10 -2
  36. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  37. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  38. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  39. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  40. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  41. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  42. data/lib/contrast/agent/version.rb +1 -1
  43. data/lib/contrast/components/config.rb +4 -4
  44. data/lib/contrast/components/protect.rb +11 -1
  45. data/lib/contrast/components/sampling.rb +15 -10
  46. data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
  47. data/lib/contrast/config/yaml_file.rb +8 -0
  48. data/lib/contrast/framework/rails/support.rb +3 -0
  49. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  50. data/lib/contrast/utils/metrics_hash.rb +1 -1
  51. data/lib/contrast/utils/object_share.rb +2 -1
  52. data/lib/contrast/utils/response_utils.rb +12 -0
  53. data/lib/contrast/utils/timer.rb +2 -0
  54. data/lib/contrast.rb +9 -2
  55. data/ruby-agent.gemspec +1 -1
  56. metadata +21 -6
  57. data/lib/contrast/utils/input_classification_base.rb +0 -169
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a9469e29e067a8c67e64a771b3d60522c4999ecd1133fdc96c3f6c9ff528f71
4
- data.tar.gz: 8cbed6cbdf60a9017c80f0edbd5bbe11ea104620ae606f2eac5db4d2c930667f
3
+ metadata.gz: e64852411fae5dd5c52973361e7f54cdf60813105423ed81cd6455c6ec212330
4
+ data.tar.gz: d7d6a1ef01242d97b36f1b8fc4767829d300ec2f3ce1dcb21df7fe05aebc22b1
5
5
  SHA512:
6
- metadata.gz: 3ba2a3f4887a593a5b61544ce6622df70cfe54d1a6b7fe6c4e57927714ab5b6c16ccf13b8c7cd703acb0f55e405c6bcb8e55c781e6f7171d087dc48885942a33
7
- data.tar.gz: 7d791ee31ffe0857ac51a6d26f7489baf986f3b4445cc697f202df3ab47dd77c2e484a2516ca2ed442afbfe9be02e8139dab2a866dcc0358210774a6c1e33ca9
6
+ metadata.gz: 86626808971cfc1fcd74febe8cdf2d41be668a78cc02d41d4bf7f6a488e550fef9f2a8fe3230eea7ea9ed63824d09e61956878d34d99375dd3595b77d3a9755f
7
+ data.tar.gz: 9fb15e1733095b1f7c2a9d5d4bfce96a3a2e9d5f956d77f0cd6c1fb810c44166a7bf0e2b89966a63ca80aa34514dd82e6f87e619a782d5024854d84c1ed93183
@@ -14,10 +14,13 @@ require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input
14
14
  require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload'
15
15
  require 'contrast/agent/protect/rule/path_traversal/path_traversal'
16
16
  require 'contrast/agent/protect/rule/path_traversal/path_traversal_input_classification'
17
+ require 'contrast/agent/protect/rule/input_classification/lru_cache'
18
+ require 'contrast/agent/protect/rule/input_classification/cached_result'
17
19
  require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
18
20
  require 'contrast/agent/protect/rule/xss/xss'
19
21
  require 'contrast/components/logger'
20
22
  require 'contrast/utils/object_share'
23
+ require 'contrast/agent/protect/rule/input_classification/base64_statistic'
21
24
  require 'json'
22
25
 
23
26
  module Contrast
@@ -35,6 +38,8 @@ module Contrast
35
38
  ].cs__freeze
36
39
  POSTFILTER_RULES = %w[sql-injection cmd-injection reflected-xss path-traversal nosql-injection].cs__freeze
37
40
  AGENTLIB_TIMEOUT = 5.cs__freeze
41
+ TIMEOUT_ERROR_MESSAGE = '[AgentLib] Timed out when processing InputAnalysisResult'
42
+ STANDARD_ERROR_MESSAGE = '[InputAnalyzer] Exception raise while doing input analysis:'
38
43
 
39
44
  class << self
40
45
  include Contrast::Agent::Reporting::InputType
@@ -42,6 +47,18 @@ module Contrast
42
47
  include Contrast::Utils::ObjectShare
43
48
  include Contrast::Components::Logger::InstanceMethods
44
49
 
50
+ # Cache for storing the input analysis result per rule
51
+ #
52
+ # @return [Contrast::Agent::Protect::Rule::InputClassification::LRUCache]
53
+ def lru_cache
54
+ @_lru_cache ||= Contrast::Agent::Protect::Rule::InputClassification::LRUCache.new
55
+ end
56
+
57
+ # Input decoding statistic.
58
+ def base64_statistic
59
+ @_base64_statistic ||= Contrast::Agent::Protect::Rule::InputClassification::Base64Statistic.new
60
+ end
61
+
45
62
  # This method with analyze the user input from the context of the
46
63
  # current request and return new ia with extracted input types.
47
64
  #
@@ -51,13 +68,13 @@ module Contrast
51
68
  return unless Contrast::PROTECT.enabled?
52
69
  return if request.nil?
53
70
 
54
- inputs = extract_input(request)
71
+ inputs = extract_inputs(request)
55
72
  return unless inputs
56
73
 
57
74
  input_analysis = Contrast::Agent::Reporting::InputAnalysis.new
58
75
  input_analysis.request = request
59
76
  # Save those for trigger time
60
- input_analysis.inputs = extract_input(request)
77
+ input_analysis.inputs = inputs
61
78
  input_analysis
62
79
  end
63
80
 
@@ -69,16 +86,9 @@ module Contrast
69
86
  #
70
87
  # @param request [Contrast::Agent::Request] current request context.
71
88
  # @return inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
72
- def extract_input request
89
+ def extract_inputs request
73
90
  inputs = {}
74
- inputs[BODY] = request.body
75
- inputs[COOKIE_NAME] = request.cookies.keys
76
- inputs[COOKIE_VALUE] = request.cookies.values
77
- inputs[HEADER] = request.headers
78
- inputs[PARAMETER_NAME] = request.parameters.keys
79
- inputs[PARAMETER_VALUE] = request.parameters.values
80
- inputs[QUERYSTRING] = request.query_string
81
- inputs[METHOD] = request.request_method
91
+ extract_request_inputs(inputs, request)
82
92
  extract_multipart(inputs, request)
83
93
  inputs.compact!
84
94
  inputs
@@ -86,22 +96,29 @@ module Contrast
86
96
 
87
97
  # classify input by rule
88
98
  #
89
- # @param rule_id [String] name of the rule
90
- # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from
91
- # analyze method.
92
- def input_classification_for rule_id, input_analysis
99
+ # @param rule_id [String] name of the rule.
100
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from analyze method.
101
+ # @param interval [Integer] The timeout determined for the AgentLib analysis to be performed.
102
+ def input_classification_for rule_id, input_analysis, interval: AGENTLIB_TIMEOUT
93
103
  return unless input_analysis&.inputs
94
104
  return unless (protect_rule = Contrast::PROTECT.rule(rule_id)) && protect_rule.enabled?
95
105
 
96
106
  input_analysis.inputs.each do |input_type, value|
97
107
  next if value.nil? || value.empty?
98
108
 
99
- protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
109
+ Timeout.timeout(interval) do
110
+ protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
111
+ end
100
112
  end
101
113
 
102
114
  input_analysis
103
115
  rescue StandardError => e
104
- logger.error('[INPUT_ANALYZER] Error', error: e)
116
+ if e.cs__class == Timeout::Error
117
+ log_error(rule_id, TIMEOUT_ERROR_MESSAGE, e)
118
+ else
119
+ log_error(rule_id, STANDARD_ERROR_MESSAGE, e, level: :error)
120
+ end
121
+ nil
105
122
  end
106
123
 
107
124
  # classify input by array of rules. There is a timeout for the AgentLib analysis if not set it
@@ -134,14 +151,9 @@ module Contrast
134
151
  # Check to see if rules is already triggered only for infilter:
135
152
  next if input_analysis.triggered_rules.include?(rule_id) && infilter
136
153
 
137
- Timeout.timeout(interval) do
138
- input_classification_for(rule_id, input_analysis)
139
- end
154
+ input_classification_for(rule_id, input_analysis, interval: interval)
140
155
  end
141
156
  input_analysis
142
- rescue Timeout::Error => e
143
- logger.warn('AgentLib timed out when processing InputAnalysisResult', e, ia_result)
144
- nil
145
157
  end
146
158
 
147
159
  private
@@ -158,6 +170,33 @@ module Contrast
158
170
  name = filename[DISPOSITION_NAME.to_sym]
159
171
  inputs[MULTIPART_NAME] = name if name
160
172
  end
173
+
174
+ # Extract the parameters and query string from the request context.
175
+ #
176
+ # @param inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
177
+ # @param request [Contrast::Agent::Request] current request context.
178
+ def extract_request_inputs inputs, request
179
+ inputs[BODY] = request.body
180
+ inputs[COOKIE_NAME] = request.cookies.keys
181
+ inputs[COOKIE_VALUE] = request.cookies.values
182
+ inputs[HEADER] = request.headers
183
+ inputs[METHOD] = request.request_method
184
+ inputs[PARAMETER_NAME] = request.parameters.keys
185
+ inputs[PARAMETER_VALUE] = request.parameters.values
186
+ inputs[QUERYSTRING] = request.query_string
187
+ end
188
+
189
+ # Logs any errrors that occur during the analysis
190
+ # Accepts a level parameter to determine if the error should be logged as an error or warning.
191
+ #
192
+ # @param rule_id [String] name of the rule.
193
+ def log_error rule_id, message, error, level: :error
194
+ if level == :error
195
+ logger.error(message, rule_id: rule_id, error: error)
196
+ else
197
+ logger.warn(message, rule_id: rule_id, error: error)
198
+ end
199
+ end
161
200
  end
162
201
  end
163
202
  end
@@ -5,7 +5,10 @@ require 'contrast/agent/thread/worker_thread'
5
5
  require 'contrast/agent/reporting/input_analysis/input_analysis_result'
6
6
  require 'contrast/agent/reporting/input_analysis/score_level'
7
7
  require 'contrast/agent/reporting/reporting_events/application_activity'
8
- require 'contrast/utils/input_classification_base'
8
+ require 'contrast/agent/protect/rule/input_classification/base'
9
+ require 'contrast/agent/telemetry/input_analysis_cache_event'
10
+ require 'contrast/agent/telemetry/input_analysis_encoding_event'
11
+ require 'contrast/utils/reporting/application_activity_batch_utils'
9
12
 
10
13
  module Contrast
11
14
  module Agent
@@ -15,7 +18,8 @@ module Contrast
15
18
  # Currently only includes: cmd_injection & sqli_injection rules
16
19
  class WorthWatchingInputAnalyzer < WorkerThread
17
20
  include Timeout
18
- include Contrast::Agent::Protect::Rule::InputClassificationBase
21
+ include Contrast::Agent::Protect::Rule::InputClassification::Base
22
+ include Contrast::Utils::Reporting::ApplicationActivityBatchUtils
19
23
 
20
24
  QUEUE_SIZE = 1000.cs__freeze
21
25
  AGENTLIB_TIMEOUT = 5.cs__freeze
@@ -48,8 +52,10 @@ module Contrast
48
52
  activity.attach_defend(attack_result)
49
53
  report = true
50
54
  end
51
- Contrast::Agent::Reporting::Masker.mask(activity)
52
- Contrast::Agent.reporter.send_event(activity) if report
55
+ report_activity(activity) if report
56
+ # Handle reporting of IA Cache statistics:
57
+ enqueue_cache_event(stored_ia.request)
58
+ enqueue_encoding_event(stored_ia.request)
53
59
  rescue StandardError => e
54
60
  logger.error('[WorthWatchingAnalyzer] thread could not process result because of:', e)
55
61
  end
@@ -73,6 +79,27 @@ module Contrast
73
79
 
74
80
  private
75
81
 
82
+ # After we have finished with all IA results, we need to send the cache statistics to Telemetry.
83
+ # Now the request cycle is finished and we can send the cache statistics.
84
+ #
85
+ # @param request [Contrast::Agent::Request] stored request.
86
+ def enqueue_cache_event request
87
+ return unless Contrast::Agent::Telemetry::Base.enabled?
88
+
89
+ Contrast::TELEMETRY_IA_CACHE[request.__id__] = Contrast::Agent::Protect::InputAnalyzer.
90
+ lru_cache.statistics.to_events.dup
91
+ Contrast::Agent::Protect::InputAnalyzer.lru_cache.clear_statistics
92
+ end
93
+
94
+ def enqueue_encoding_event request
95
+ return unless Contrast::Agent::Telemetry::Base.enabled?
96
+ return unless Contrast::PROTECT.normalize_base64?
97
+
98
+ Contrast::TELEMETRY_BASE64_HASH[request.__id__] = Contrast::Agent::Protect::InputAnalyzer.
99
+ base64_statistic.to_events.dup
100
+ Contrast::Agent::Protect::InputAnalyzer.base64_statistic.clear
101
+ end
102
+
76
103
  # This method will build the attack results from the saved ia.
77
104
  #
78
105
  # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis]
@@ -114,6 +141,12 @@ module Contrast
114
141
  @_queue ||= Queue.new
115
142
  end
116
143
 
144
+ def report_activity activity
145
+ logger.debug('[WorthWatchingAnalyzer] preparing to send activity batch')
146
+ add_activity_to_batch(activity)
147
+ report_batch
148
+ end
149
+
117
150
  def delete_queue!
118
151
  @_queue&.clear
119
152
  @_queue&.close
@@ -63,6 +63,10 @@ module Contrast
63
63
  RULE_NAME
64
64
  end
65
65
 
66
+ # Should return the short name.
67
+ #
68
+ # @return [String]
69
+
66
70
  # Should return list of all sub_rules.
67
71
  # Extend for each main rule any sub-rules.
68
72
  #
@@ -328,7 +332,7 @@ module Contrast
328
332
  # @param context [Contrast::Agent::RequestContext]
329
333
  # @return [Array<Contrast::Agent::Reporting::InputAnalysis>]
330
334
  def gather_ia_results context
331
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless context&.agent_input_analysis&.results
335
+ return [] unless context&.agent_input_analysis&.results
332
336
 
333
337
  context.agent_input_analysis.results.select do |ia_result|
334
338
  ia_result.rule_id == rule_name && ia_result.score_level != Contrast::Agent::Reporting::ScoreLevel::IGNORE
@@ -4,7 +4,7 @@
4
4
  require 'contrast/agent/reporting/input_analysis/input_type'
5
5
  require 'contrast/agent/reporting/input_analysis/score_level'
6
6
  require 'contrast/agent/reporting/details/bot_blocker_details'
7
- require 'contrast/utils/input_classification_base'
7
+ require 'contrast/agent/protect/rule/input_classification/base'
8
8
  require 'contrast/utils/object_share'
9
9
 
10
10
  module Contrast
@@ -20,7 +20,7 @@ module Contrast
20
20
  BOT_BLOCKER_MATCH = 'bot-blocker-input-tracing-v1'
21
21
 
22
22
  class << self
23
- include InputClassificationBase
23
+ include Contrast::Agent::Protect::Rule::InputClassification::Base
24
24
 
25
25
  # Input Classification stage is done to determine if an user input is
26
26
  # DEFINITEATTACK or to be ignored.
@@ -45,6 +45,7 @@ module Contrast
45
45
  input_analysis
46
46
  rescue StandardError => e
47
47
  logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
48
+ nil
48
49
  end
49
50
 
50
51
  private
@@ -57,19 +58,35 @@ module Contrast
57
58
  # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
58
59
  # @param value [String, Array<String>] the value of the input.
59
60
  #
60
- # @return res [Contrast::Agent::Reporting::InputAnalysisResult]
61
+ # @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil]
61
62
  def create_new_input_result request, rule_id, input_type, value
62
63
  return unless request.headers.key(value) == USER_AGENT
63
- return unless Contrast::AGENT_LIB
64
64
 
65
- # If there is no match this would return nil.
66
- header_eval = Contrast::AGENT_LIB.eval_header(AGENT_LIB_HEADER_NAME,
67
- value,
68
- Contrast::AGENT_LIB.rule_set[rule_id],
69
- Contrast::AGENT_LIB.eval_option[:NONE])
65
+ super(request, rule_id, input_type, value)
66
+ end
67
+
68
+ # Creates new instance of AgentLib evaluation result with direct call to AgentLib.
69
+ #
70
+ # @param rule_id [String] The name of the Protect Rule.
71
+ # @param _input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
72
+ # @param value [String, Array<String>] the value of the input.
73
+ def build_input_eval rule_id, _input_type, value
74
+ Contrast::AGENT_LIB.eval_header(AGENT_LIB_HEADER_NAME,
75
+ value,
76
+ Contrast::AGENT_LIB.rule_set[rule_id],
77
+ Contrast::AGENT_LIB.eval_option[:NONE])
78
+ end
70
79
 
80
+ # Creates specific result from the AgentLib evaluation.
81
+ #
82
+ # @param rule_id [String] The name of the Protect Rule.
83
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
84
+ # @param value [String, Array<String>] the value of the input.
85
+ # @param request [Contrast::Agent::Request] the current request context.
86
+ # @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
87
+ def build_ia_result rule_id, input_type, value, request, input_eval
71
88
  ia_result = new_ia_result(rule_id, input_type, request.path, value)
72
- score = header_eval&.score || 0
89
+ score = input_eval&.score || 0
73
90
  if score >= THRESHOLD
74
91
  ia_result.score_level = DEFINITEATTACK
75
92
  ia_result.ids << BOT_BLOCKER_MATCH
@@ -79,7 +96,6 @@ module Contrast
79
96
  else
80
97
  ia_result.score_level = IGNORE
81
98
  end
82
- add_needed_key(request, ia_result, input_type, value)
83
99
  ia_result
84
100
  end
85
101
 
@@ -43,7 +43,6 @@ module Contrast
43
43
  # to BLOCK and valid cdmi is detected.
44
44
  def infilter context, classname, method, command
45
45
  return unless infilter?(command)
46
- return if protect_excluded_by_url?(rule_name)
47
46
  return unless (result = build_violation(context, command))
48
47
 
49
48
  append_to_activity(context, result)
@@ -4,7 +4,7 @@
4
4
  require 'contrast/agent/protect/rule/cmdi/cmd_injection'
5
5
  require 'contrast/agent/reporting/input_analysis/score_level'
6
6
  require 'contrast/agent/protect/input_analyzer/input_analyzer'
7
- require 'contrast/utils/input_classification_base'
7
+ require 'contrast/agent/protect/rule/input_classification/base'
8
8
  require 'contrast/components/logger'
9
9
 
10
10
  module Contrast
@@ -17,7 +17,7 @@ module Contrast
17
17
  module CmdiInputClassification
18
18
  WORTHWATCHING_MATCH = 'cmdi-worth-watching-v2'.cs__freeze
19
19
  class << self
20
- include InputClassificationBase
20
+ include Contrast::Agent::Protect::Rule::InputClassification::Base
21
21
  include Contrast::Components::Logger::InstanceMethods
22
22
  end
23
23
  end
@@ -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