contrast-agent 7.2.0 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
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