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
@@ -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
@@ -0,0 +1,23 @@
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
+ module Contrast
5
+ module Agent
6
+ module Protect
7
+ module Rule
8
+ module InputClassification
9
+ # Utils module for Input Classification.
10
+ module Utils
11
+ # Extracts data return as array from the cache.
12
+ #
13
+ # @param object [NilClass, Array<Contrast::Agent::Protect::InputClassification::CachedResult>]
14
+ # @return object [Contrast::Agent::Protect::InputClassification::CachedResult]
15
+ def safe_extract object
16
+ Array(object)[0]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -4,7 +4,7 @@
4
4
  require 'contrast/utils/object_share'
5
5
  require 'contrast/agent/protect/rule/no_sqli/no_sqli'
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
 
9
9
  module Contrast
10
10
  module Agent
@@ -15,7 +15,7 @@ module Contrast
15
15
  # to be analyzed at the sink level.
16
16
  module NoSqliInputClassification
17
17
  class << self
18
- include InputClassificationBase
18
+ include Contrast::Agent::Protect::Rule::InputClassification::Base
19
19
 
20
20
  NOSQL_COMMENT_REGEXP = %r{"\s*(?:<--|//)}.cs__freeze
21
21
  NOSQL_OR_REGEXP = /(?=(\s+\|\|\s+))/.cs__freeze
@@ -96,8 +96,15 @@ module Contrast
96
96
  #
97
97
  # @return res [Contrast::Agent::Reporting::InputAnalysisResult]
98
98
  def create_new_input_result request, rule_id, input_type, value
99
- score = evaluate_patterns(value)
100
- score = evaluate_rules(value, score)
99
+ return unless Contrast::AGENT_LIB
100
+
101
+ # Cache retrieve
102
+ cached = Contrast::Agent::Protect::InputAnalyzer.lru_cache.lookout(rule_id, value, input_type, request)
103
+ return cached.result if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
104
+
105
+ eval_value = base64_decode_input(value, input_type)
106
+ score = evaluate_patterns(eval_value)
107
+ score = evaluate_rules(eval_value, score)
101
108
 
102
109
  score_level = if definite_attack?(score)
103
110
  DEFINITEATTACK
@@ -106,9 +113,12 @@ module Contrast
106
113
  else
107
114
  IGNORE
108
115
  end
109
- result = new_ia_result(rule_id, input_type, score_level, request.path, value)
110
- add_needed_key(request, result, input_type, value)
111
- result
116
+ ia_result = new_ia_result(rule_id, input_type, score_level, request.path, value)
117
+ add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
118
+
119
+ # Cache save. Cache must be saved after the input evaluation is completed.
120
+ Contrast::Agent::Protect::InputAnalyzer.lru_cache.save(rule_id, ia_result, request)
121
+ ia_result
112
122
  end
113
123
 
114
124
  # This method evaluates the patterns relevant to NoSQL Injection to check whether
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'contrast/utils/input_classification_base'
4
+ require 'contrast/agent/protect/rule/input_classification/base'
5
5
 
6
6
  module Contrast
7
7
  module Agent
@@ -15,27 +15,31 @@ module Contrast
15
15
 
16
16
  THRESHOLD = 90.cs__freeze
17
17
  class << self
18
- include InputClassificationBase
18
+ include Contrast::Agent::Protect::Rule::InputClassification::Base
19
19
 
20
20
  private
21
21
 
22
- # This methods checks if input is tagged DEFINITEATTACK or IGNORE matches value with it's
23
- # key if needed and Creates new isntance of InputAnalysisResult.
22
+ # Creates new instance of AgentLib evaluation result with direct call to AgentLib.
24
23
  #
25
- # @param request [Contrast::Agent::Request] the current request context.
26
24
  # @param rule_id [String] The name of the Protect Rule.
27
25
  # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
28
26
  # @param value [String, Array<String>] the value of the input.
29
- #
30
- # @return res [Contrast::Agent::Reporting::InputAnalysisResult]
31
- def create_new_input_result request, rule_id, input_type, value
32
- return unless Contrast::AGENT_LIB
33
-
34
- input_eval = Contrast::AGENT_LIB.eval_input(value,
35
- convert_input_type(input_type),
36
- Contrast::AGENT_LIB.rule_set[rule_id],
37
- Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
27
+ def build_input_eval rule_id, input_type, value
28
+ Contrast::AGENT_LIB.eval_input(value,
29
+ Contrast::Agent::Protect::Rule::InputClassification::Base.
30
+ convert_input_type(input_type),
31
+ Contrast::AGENT_LIB.rule_set[rule_id],
32
+ Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
33
+ end
38
34
 
35
+ # Creates specific result from the AgentLib evaluation.
36
+ #
37
+ # @param rule_id [String] The name of the Protect Rule.
38
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
39
+ # @param value [String, Array<String>] the value of the input.
40
+ # @param request [Contrast::Agent::Request] the current request context.
41
+ # @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
42
+ def build_ia_result rule_id, input_type, value, request, input_eval
39
43
  ia_result = new_ia_result(rule_id, input_type, request.path, value)
40
44
  score = input_eval&.score || 0
41
45
  if score >= THRESHOLD
@@ -50,7 +54,6 @@ module Contrast
50
54
  else
51
55
  ia_result.score_level = IGNORE
52
56
  end
53
- add_needed_key(request, ia_result, input_type, value)
54
57
  ia_result
55
58
  end
56
59
  end
@@ -5,7 +5,7 @@ require 'contrast/utils/object_share'
5
5
  require 'contrast/agent/reporting/input_analysis/input_type'
6
6
  require 'contrast/agent/reporting/input_analysis/score_level'
7
7
  require 'contrast/agent/protect/input_analyzer/input_analyzer'
8
- require 'contrast/utils/input_classification_base'
8
+ require 'contrast/agent/protect/rule/input_classification/base'
9
9
 
10
10
  module Contrast
11
11
  module Agent
@@ -17,7 +17,7 @@ module Contrast
17
17
  module SqliInputClassification
18
18
  WORTHWATCHING_MATCH = 'sqli-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
@@ -4,7 +4,7 @@
4
4
  require 'contrast/utils/object_share'
5
5
  require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload'
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
 
9
9
  module Contrast
10
10
  module Agent
@@ -16,26 +16,30 @@ module Contrast
16
16
  UNSAFE_UPLOAD_MATCH = 'unsafe-file-upload-input-tracing-v1'
17
17
 
18
18
  class << self
19
- include InputClassificationBase
19
+ include Contrast::Agent::Protect::Rule::InputClassification::Base
20
20
 
21
21
  private
22
22
 
23
- # This methods checks if input is tagged DEFINITEATTACK or IGNORE matches value with it's
24
- # key if needed and Creates new isntance of InputAnalysisResult.
23
+ # Creates new instance of AgentLib evaluation result with direct call to AgentLib.
25
24
  #
26
- # @param request [Contrast::Agent::Request] the current request context.
27
25
  # @param rule_id [String] The name of the Protect Rule.
28
- # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
26
+ # @param _input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
29
27
  # @param value [String, Array<String>] the value of the input.
30
- #
31
- # @return res [Contrast::Agent::Reporting::InputAnalysisResult]
32
- def create_new_input_result request, rule_id, input_type, value
33
- return unless Contrast::AGENT_LIB
34
- return unless (input_eval = Contrast::AGENT_LIB.eval_input(value,
35
- Contrast::AGENT_LIB.input_set[:MULTIPART_NAME],
36
- Contrast::AGENT_LIB.rule_set[rule_id],
37
- Contrast::AGENT_LIB.eval_option[:NONE]))
28
+ def build_input_eval rule_id, _input_type, value
29
+ Contrast::AGENT_LIB.eval_input(value,
30
+ Contrast::AGENT_LIB.input_set[:MULTIPART_NAME],
31
+ Contrast::AGENT_LIB.rule_set[rule_id],
32
+ Contrast::AGENT_LIB.eval_option[:NONE])
33
+ end
38
34
 
35
+ # Creates specific result from the AgentLib evaluation.
36
+ #
37
+ # @param rule_id [String] The name of the Protect Rule.
38
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
39
+ # @param value [String, Array<String>] the value of the input.
40
+ # @param request [Contrast::Agent::Request] the current request context.
41
+ # @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
42
+ def build_ia_result rule_id, input_type, value, request, input_eval
39
43
  ia_result = new_ia_result(rule_id, input_type, request.path, value)
40
44
  if input_eval.score >= THRESHOLD
41
45
  ia_result.score_level = DEFINITEATTACK
@@ -51,7 +55,6 @@ module Contrast
51
55
  else
52
56
  Contrast::Utils::ObjectShare::EMPTY_STRING
53
57
  end
54
-
55
58
  ia_result
56
59
  end
57
60
  end
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'contrast/utils/input_classification_base'
4
+ require 'contrast/agent/protect/rule/input_classification/base'
5
5
 
6
6
  module Contrast
7
7
  module Agent
@@ -13,28 +13,32 @@ module Contrast
13
13
  REFLECTED_XSS_MATCH = 'reflected-xss-input-tracing-v1'.cs__freeze
14
14
  WORTHWATCHING_MATCH = 'xss-worth-watching-v2'.cs__freeze
15
15
  class << self
16
- include InputClassificationBase
16
+ include Contrast::Agent::Protect::Rule::InputClassification::Base
17
17
 
18
18
  private
19
19
 
20
- # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
21
- # key if needed and Creates new isntance of InputAnalysisResult.
20
+ # Creates new instance of AgentLib evaluation result with direct call to AgentLib.
22
21
  #
23
- # @param request [Contrast::Agent::Request] the current request context.
24
22
  # @param rule_id [String] The name of the Protect Rule.
25
23
  # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
26
24
  # @param value [String, Array<String>] the value of the input.
27
- #
28
- # @return res [Contrast::Agent::Reporting::InputAnalysisResult]
29
- def create_new_input_result request, rule_id, input_type, value
30
- return unless Contrast::AGENT_LIB
31
-
32
- input_eval = Contrast::AGENT_LIB.eval_input(value,
33
- convert_input_type(input_type),
34
- Contrast::AGENT_LIB.rule_set[rule_id],
35
- Contrast::AGENT_LIB.
36
- eval_option[:PREFER_WORTH_WATCHING])
25
+ def build_input_eval rule_id, input_type, value
26
+ Contrast::AGENT_LIB.eval_input(value,
27
+ Contrast::Agent::Protect::Rule::InputClassification::Base.
28
+ convert_input_type(input_type),
29
+ Contrast::AGENT_LIB.rule_set[rule_id],
30
+ Contrast::AGENT_LIB.
31
+ eval_option[:PREFER_WORTH_WATCHING])
32
+ end
37
33
 
34
+ # Creates specific result from the AgentLib evaluation.
35
+ #
36
+ # @param rule_id [String] The name of the Protect Rule.
37
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
38
+ # @param value [String, Array<String>] the value of the input.
39
+ # @param request [Contrast::Agent::Request] the current request context.
40
+ # @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
41
+ def build_ia_result rule_id, input_type, value, request, input_eval
38
42
  score = input_eval&.score || 0
39
43
  ia_result = new_ia_result(rule_id, input_type, request.path, value)
40
44
  if score >= THRESHOLD
@@ -46,8 +50,6 @@ module Contrast
46
50
  else
47
51
  ia_result.score_level = IGNORE
48
52
  end
49
-
50
- add_needed_key(request, ia_result, input_type, value)
51
53
  ia_result
52
54
  end
53
55
  end
@@ -5,6 +5,7 @@ require 'contrast/utils/object_share'
5
5
  require 'contrast/utils/timer'
6
6
  require 'contrast/agent/reporting/attack_result/response_type'
7
7
  require 'contrast/agent/reporting/attack_result/rasp_rule_sample'
8
+ require 'contrast/utils/duck_utils'
8
9
 
9
10
  module Contrast
10
11
  module Agent
@@ -65,6 +66,11 @@ module Contrast
65
66
  def details= protect_details
66
67
  @_details = protect_details if protect_details.is_a?(Contrast::Agent::Reporting::Details::ProtectRuleDetails)
67
68
  end
69
+
70
+ def empty?
71
+ Contrast::Utils::DuckUtils.empty_duck?(samples) || Contrast::Utils::DuckUtils.empty_duck?(rule_id) ||
72
+ response == ::Contrast::Agent::Reporting::ResponseType::NO_ACTION
73
+ end
68
74
  end
69
75
  end
70
76
  end
@@ -9,13 +9,8 @@ module Contrast
9
9
  module Reporting
10
10
  # This class will do ia analysis for our protect rules
11
11
  class InputAnalysis
12
- def inputs
13
- @_inputs
14
- end
15
-
16
- def inputs= extracted_inputs
17
- @_inputs = extracted_inputs
18
- end
12
+ # @return [Hash] Stored request inputs for this context.
13
+ attr_accessor :inputs
19
14
 
20
15
  def triggered_rules
21
16
  @_triggered_rules ||= []
@@ -61,6 +61,17 @@ module Contrast
61
61
  @_key = key if key.is_a?(String)
62
62
  end
63
63
 
64
+ # @param key_type [Symbol]
65
+ # @return @_key [String]
66
+ def key_type= key_type
67
+ @_key_type = key_type if INPUT_TYPE.to_a.include?(key_type)
68
+ end
69
+
70
+ # @return @_key [String]
71
+ def key_type
72
+ @_key_type ||= INPUT_TYPE::UNDEFINED_TYPE
73
+ end
74
+
64
75
  # @return value [String]
65
76
  def value
66
77
  @_value ||= Contrast::Utils::ObjectShare::EMPTY_STRING
@@ -30,13 +30,45 @@ module Contrast
30
30
  UNKNOWN = :UNKNOWN.cs__freeze
31
31
 
32
32
  class << self
33
+ # @return
33
34
  def to_a
34
- [
35
+ @_to_a ||= [
35
36
  UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME, PARAMETER_VALUE,
36
37
  QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE, MULTIPART_VALUE,
37
38
  MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST, URL_PARAMETER, UNKNOWN
38
39
  ]
39
40
  end
41
+
42
+ # This is a hash of the input types and their corresponding values.
43
+ #
44
+ # @return [Hash]
45
+
46
+ def to_hash
47
+ {
48
+ UNDEFINED_TYPE: '1',
49
+ BODY: '2',
50
+ COOKIE_NAME: '3',
51
+ COOKIE_VALUE: '4',
52
+ HEADER: '5',
53
+ PARAMETER_NAME: '6',
54
+ PARAMETER_VALUE: '7',
55
+ QUERYSTRING: '8',
56
+ URI: '9',
57
+ SOCKET: '10',
58
+ JSON_VALUE: '11',
59
+ JSON_ARRAYED_VALUE: '12',
60
+ MULTIPART_CONTENT_TYPE: '13',
61
+ MULTIPART_VALUE: '14',
62
+ MULTIPART_FIELD_NAME: '15',
63
+ MULTIPART_NAME: '16',
64
+ XML_VALUE: '17',
65
+ DWR_VALUE: '18',
66
+ METHOD: '19',
67
+ REQUEST: '20',
68
+ URL_PARAMETER: '21',
69
+ UNKNOWN: '22'
70
+ }
71
+ end
40
72
  end
41
73
  end
42
74
  end
@@ -23,7 +23,7 @@ module Contrast
23
23
  hash = URI.decode_www_form(query).to_h
24
24
  mask_with_dictionary(results, hash)
25
25
  # Restore to string form.
26
- hash.each { |k, v| masked += "#{ k }=#{ v }&" }
26
+ hash.each { |k, v| masked += "#{ k }#{ EQUALS }#{ v }#{ AMPERSAND }" }
27
27
  query = masked
28
28
  query.chomp!(masked[-1])
29
29
  end
@@ -40,6 +40,7 @@ module Contrast
40
40
  # @param attack_result [Contrast::Agent::Reporting::AttackResult]
41
41
  def attach_data attack_result
42
42
  return unless attack_result&.cs__is_a?(Contrast::Agent::Reporting::AttackResult)
43
+ return if attack_result&.empty?
43
44
 
44
45
  attacker_activity = Contrast::Agent::Reporting::ApplicationDefendAttackerActivity.new(ia_request: @request)
45
46
  attacker_activity.attach_data(attack_result)
@@ -5,6 +5,7 @@ require 'contrast/components/logger'
5
5
  require 'contrast/utils/object_share'
6
6
  require 'contrast/utils/duck_utils'
7
7
  require 'contrast/agent/reporting/reporting_events/reportable_hash'
8
+ require 'contrast/agent/reporting/attack_result/response_type'
8
9
  require 'contrast/agent/reporting/reporting_events/application_defend_attack_activity'
9
10
 
10
11
  module Contrast
@@ -118,7 +118,7 @@ module Contrast
118
118
  mode.resend.reset_rescue_attempts
119
119
  findings_to_return.each do |index|
120
120
  preflight_message = event.messages[index.to_i]
121
- corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message.data)
121
+ corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message&.data)
122
122
  next unless corresponding_finding
123
123
 
124
124
  send_event(corresponding_finding, connection)
@@ -143,6 +143,8 @@ module Contrast
143
143
  super
144
144
  delete_queue!
145
145
  Contrast::TELEMETRY_EXCEPTIONS&.clear
146
+ Contrast::TELEMETRY_IA_CACHE&.clear
147
+ Contrast::TELEMETRY_BASE64_HASH&.clear
146
148
  end
147
149
 
148
150
  private
@@ -174,8 +176,7 @@ module Contrast
174
176
  break unless attempt_to_start?
175
177
 
176
178
  # Start pushing exceptions to queue for reporting.
177
- Contrast::TELEMETRY_EXCEPTIONS&.each_value { |value| queue << value }
178
- Contrast::TELEMETRY_EXCEPTIONS&.clear
179
+ gather_telemetry_events
179
180
  until queue.empty?
180
181
  event = queue.pop
181
182
  begin
@@ -189,6 +190,31 @@ module Contrast
189
190
  end
190
191
  end
191
192
  end
193
+
194
+ # Fills the queue with events that were not able to be sent previously.
195
+ def gather_telemetry_events
196
+ gather_exceptions
197
+ gather_encoding_events
198
+ gather_ia_cache_events
199
+ end
200
+
201
+ # Retrieves the exceptions that were accumulated.
202
+ def gather_exceptions
203
+ Contrast::TELEMETRY_EXCEPTIONS&.each_value { |value| queue << value }
204
+ Contrast::TELEMETRY_EXCEPTIONS&.clear
205
+ end
206
+
207
+ # Retrieves the base64 encoded events that were accumulated.
208
+ def gather_encoding_events
209
+ Contrast::TELEMETRY_BASE64_HASH&.each_value { |values| values.each { |event| queue << event } }
210
+ Contrast::TELEMETRY_BASE64_HASH&.clear
211
+ end
212
+
213
+ # Retrieves the IA cache events that were accumulated.
214
+ def gather_ia_cache_events
215
+ Contrast::TELEMETRY_IA_CACHE&.each_value { |values| values.each { |event| queue << event } }
216
+ Contrast::TELEMETRY_IA_CACHE&.clear
217
+ end
192
218
  end
193
219
  end
194
220
  end
@@ -0,0 +1,55 @@
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/telemetry/input_analysis_encoding_event'
5
+
6
+ module Contrast
7
+ module Agent
8
+ module Telemetry
9
+ # This hash will store the telemetry data for the Protect InputAnalysis cache.
10
+ class Base64Hash < Hash
11
+ include Contrast::Components::Logger::InstanceMethods
12
+ # Set per request:
13
+ HASH_SIZE_LIMIT = 100
14
+
15
+ # Wrapper to set a value in this Telemetry::Hash only if the provided value is of the data_type for this
16
+ # Telemetry::CacheHash or the hash has not reached its limit for unique keys.
17
+ # Saves Array of reportable events.
18
+ #
19
+ # @param key [Object] the key to which to associate the value
20
+ # @param events [array<Object>]
21
+ # @return [Object, nil] echo back out the value as the Hash#[]= method does, or nil if not of the expected
22
+ # data_type
23
+ def []= key, events
24
+ # If telemetry is not running, do not add more as we want to avoid a memory leak.
25
+ return unless Contrast::Agent.telemetry_queue&.running?
26
+ # If the Hash is full, do not add more as we want to avoid consuming all application resources.
27
+ return if at_limit?
28
+ # If the given value is of unexpected type, do not add it to avoid issues later where type is assumed.
29
+ return unless valid_event?(events)
30
+
31
+ super(key, events)
32
+ end
33
+
34
+ # Determine if hash has reached exception event limit.
35
+ #
36
+ # @return [Boolean]
37
+ def at_limit?
38
+ unless length < HASH_SIZE_LIMIT
39
+ logger.debug("[Telemetry] Number of IA base64 events exceeds limit of #{ HASH_SIZE_LIMIT }")
40
+ return true
41
+ end
42
+ false
43
+ end
44
+
45
+ private
46
+
47
+ # Checks to see if the given object is a valid event.
48
+ # @param events [Contrast::Agent::Telemetry::InputAnalysisEncodingEvent]
49
+ def valid_event? events
50
+ events&.all?(Contrast::Agent::Telemetry::InputAnalysisEncodingEvent)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end