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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a9469e29e067a8c67e64a771b3d60522c4999ecd1133fdc96c3f6c9ff528f71
4
- data.tar.gz: 8cbed6cbdf60a9017c80f0edbd5bbe11ea104620ae606f2eac5db4d2c930667f
3
+ metadata.gz: 4223bb2218df4bdf98b2600c77c4fa148e71b830575e938a968d41e89138fb81
4
+ data.tar.gz: cc559a6b364c5019d9c50d8ee6b8237486ac1596ec8ade76d8e6ae4e51e1b906
5
5
  SHA512:
6
- metadata.gz: 3ba2a3f4887a593a5b61544ce6622df70cfe54d1a6b7fe6c4e57927714ab5b6c16ccf13b8c7cd703acb0f55e405c6bcb8e55c781e6f7171d087dc48885942a33
7
- data.tar.gz: 7d791ee31ffe0857ac51a6d26f7489baf986f3b4445cc697f202df3ab47dd77c2e484a2516ca2ed442afbfe9be02e8139dab2a866dcc0358210774a6c1e33ca9
6
+ metadata.gz: 6b88f94ad140a8dd0e8099e2d4f3a396f2221de6ba9a3cbdfaa7e9327644703b7dc275c369ffe4efa29109619bf9eb182f2a0f7b85103c67e2a83246c49c137a
7
+ data.tar.gz: eaf65ba9546f37ace248a29657e690966deaf02aa26426b169f1f3e9639aa3f7b355ea858afd4ef0a5787bd9e2549d7927b3db89894eb6ef4bcd26daf01382e9
@@ -16,6 +16,7 @@ module Contrast
16
16
  class PolicyNode < Contrast::Agent::Patching::Policy::PolicyNode
17
17
  include Contrast::Components::Logger::InstanceMethods
18
18
  include PolicyNodeUtils
19
+
19
20
  JSON_TAGS = 'tags'
20
21
  JSON_DATAFLOW = 'dataflow'
21
22
  # The keys used to read from policy.json to create the individual
@@ -48,6 +49,9 @@ module Contrast
48
49
  ].cs__freeze
49
50
  TO_S = %w[to_s to_str].cs__freeze
50
51
 
52
+ # Here are all Responses that will be tracked as sources, or methods they use, like body.
53
+ RESPONSE_SOURCES = %w[Net::HTTPResponse Rack::Response Sinatra::Response].cs__freeze
54
+
51
55
  def initialize policy_hash = {}
52
56
  super(policy_hash)
53
57
  @source_string = policy_hash[JSON_SOURCE]
@@ -57,13 +61,14 @@ module Contrast
57
61
  @targets = convert_policy_markers(target_string)
58
62
  @_use_original_object = ORIGINAL_OBJECT_METHODS.include?(@method_name)
59
63
  @_use_original_on_bang_method = assign_on_bang_check(policy_hash)
64
+ @_use_response_as_source = RESPONSE_SOURCES.include?(@class_name)
60
65
  end
61
66
 
67
+ # If we have KEEP action on String, and the method is to_s, that method would return self:
68
+ # String#to_s => self or string. This method is included here to cover the situations such as
69
+ # String.to_s.html_safe, where normally the dynamic sources properties get lost. To solve this
70
+ # we will simply return the original object here.
62
71
  def assign_on_bang_check policy_hash
63
- # If we have KEEP action on String, and the method is to_s, that method would return self:
64
- # String#to_s => self or string. This method is included here to cover the situations such as
65
- # String.to_s.html_safe, where normally the dynamic sources properties get lost. To solve this
66
- # we will simply return the original object here.
67
72
  return true if @_use_original_object && TO_S.include?(policy_hash[JSON_METHOD_NAME])
68
73
 
69
74
  @_use_original_object &&
@@ -166,7 +171,7 @@ module Contrast
166
171
  # that the method is without bang - it does not change the source, but rather
167
172
  # creates a copy of it.
168
173
  #
169
- # @return true | false
174
+ # @return [Boolean]
170
175
  def use_original_object?
171
176
  @_use_original_object && Contrast::ASSESS.track_original_object?
172
177
  end
@@ -175,10 +180,24 @@ module Contrast
175
180
  # that the target return is the same as object - a bang method modifying the
176
181
  # source.
177
182
  #
178
- # @return true | false
183
+ # @return [Boolean]
179
184
  def use_original_on_bang_method?
180
185
  @_use_original_on_bang_method && Contrast::ASSESS.track_original_object?
181
186
  end
187
+
188
+ # This method will check if policy is fit to use response as source.
189
+ #
190
+ # @return [Boolean]
191
+ def use_response_as_source?
192
+ Contrast::ASSESS.track_response_as_source?
193
+ end
194
+
195
+ # This method will check if the policy node is for response method.
196
+ #
197
+ # @return [Boolean]
198
+ def response_source_node?
199
+ @_use_response_as_source
200
+ end
182
201
  end
183
202
  end
184
203
  end
@@ -0,0 +1,64 @@
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/assess/policy/propagator/select'
5
+ require 'contrast/utils/duck_utils'
6
+
7
+ module Contrast
8
+ module Agent
9
+ module Assess
10
+ module Policy
11
+ module Propagator
12
+ # Propagation that results in all the tags of the source being
13
+ # applied to the target at the point of insertion. The target's
14
+ # preexisting tags are shifted to account for this insertion.
15
+ class Response < Contrast::Agent::Assess::Policy::Propagator::Base
16
+ class << self
17
+ # This will path the Net::HTTP.request method. It takes two parameters:
18
+ # - req: Net::HTTPGenericRequest
19
+ # - body: String
20
+ # As body may be optional, we need to check if it's nil or not.
21
+ #
22
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode]
23
+ # @param preshift [Contrast::Agent::Assess::Preshift]
24
+ # @param ret [Object] Return targer from method invocation.
25
+ # @param _block [nil, {}] block passed.
26
+ def net_response_keep propagation_node, preshift, ret, _block
27
+ return unless Contrast::ASSESS.track_response_as_source?
28
+
29
+ # Check to see if the argument is of correct type, and whether the body is tracked or not.
30
+ # if it's tracked and the body is not nil, then copy the properties from the source's body
31
+ # to the target's body.
32
+ source_body = if preshift.args.length == 2
33
+ preshift.args[1]
34
+ else
35
+ preshift.args[0]&.body
36
+ end
37
+ copy_body_tags(propagation_node, source_body, ret)
38
+ end
39
+
40
+ private
41
+
42
+ # Copy the properties form source body to the response body, if one is present.
43
+ #
44
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode]
45
+ # @param source_body [String] the tracked body to copy from.
46
+ # @param ret [String] the return target from method invocation.
47
+ # @return [String, nil]
48
+ def copy_body_tags propagation_node, source_body, ret
49
+ return if Contrast::Utils::DuckUtils.empty_duck?(source_body)
50
+ return unless ret&.body&.cs__is_a?(String)
51
+ return unless source_body&.cs__is_a?(String)
52
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret.body))
53
+
54
+ # KEEP
55
+ properties.copy_from(source_body, ret.body, 0, propagation_node.untags)
56
+ ret
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -31,6 +31,7 @@ module Contrast
31
31
  require 'contrast/agent/assess/policy/propagator/substitution'
32
32
  require 'contrast/agent/assess/policy/propagator/trim'
33
33
  require 'contrast/agent/assess/policy/propagator/buffer'
34
+ require 'contrast/agent/assess/policy/propagator/response'
34
35
  end
35
36
  end
36
37
  end
@@ -46,6 +46,11 @@ module Contrast
46
46
  # Exclusions makes method slow:
47
47
  return if excluded_by_url?
48
48
 
49
+ # Check to see if the source node is to be used for response as source.
50
+ if method_policy.source_node.response_source_node? && !method_policy.source_node.use_response_as_source?
51
+ return
52
+ end
53
+
49
54
  # used to hold the object and ret
50
55
  source_data = Contrast::Agent::Assess::Events::EventData.new(nil, nil, object, ret, nil)
51
56
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'rack'
5
5
  require 'contrast/utils/hash_digest'
6
+ require 'contrast/utils/duck_utils'
6
7
  require 'contrast/utils/string_utils'
7
8
  require 'contrast/agent/assess/rule/response/base_rule'
8
9
 
@@ -44,21 +45,35 @@ module Contrast
44
45
  # @param element_start_str [String] element to find in html section
45
46
  # @return [Array<Hash>] the found elements of this section, as well as their start and end indexes.
46
47
  def html_elements section, element_start_str = '', capture_overflow: false
48
+ return [] unless section
49
+ return [] unless (potentials = potential_elements(section, element_start_str).flatten).any?
50
+
47
51
  elements = []
48
52
  section_start = 0
49
- return [] unless section
50
53
 
51
- potential_elements(section, element_start_str).flatten.each do |potential_element|
54
+ potentials.each do |potential_element|
52
55
  next unless potential_element
53
56
  next unless element_openings.any? { |opening| potential_element.start_with?(opening) }
54
57
 
55
- section_start = section.index(element_start_str, section_start)
56
- next unless section_start
58
+ start = section&.index(element_start_str, section_start)
59
+ next if Contrast::Utils::DuckUtils.empty_duck?(start)
60
+
61
+ stop = potential_element.index('>').to_i
62
+ next if Contrast::Utils::DuckUtils.empty_duck?(stop)
57
63
 
58
- element_stop = potential_element.index('>').to_i
59
- next unless element_stop
64
+ section_close = start + 6 + stop
65
+ # Now we have valid tag section with start and stop.
66
+ # Save new boundaries. This is to make sure that If
67
+ # on previous iteration there were non valid section,
68
+ # the start_section will be assigned to nil, thus making
69
+ # the detection of new section not possible, and throwing
70
+ # an error. To that end old values are kept safe.
71
+ #
72
+ # Assign new start index.
73
+ section_start = start
74
+ # Assign new end index.
75
+ element_stop = stop
60
76
 
61
- section_close = section_start + 6 + element_stop
62
77
  elements << capture(section, section_start, section_close, element_stop, overflow: capture_overflow)
63
78
  section_start = section_close
64
79
  end
@@ -70,7 +70,10 @@ module Contrast
70
70
  # @param response [Contrast::Agent::Response] the response of the application
71
71
  # @return [Array<Hash<String,String>]
72
72
  def cache_meta_tags response
73
- html_elements(response.body&.split(HEAD_TAG)&.last, META_START_STR).
73
+ head_tag = response.body&.split(HEAD_TAG)&.last
74
+ return [] unless head_tag
75
+
76
+ html_elements(head_tag, META_START_STR, capture_overflow: false).
74
77
  select { |tag| cache_control_tag?(tag[HTML_PROP]) }
75
78
  end
76
79
 
@@ -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