contrast-agent 7.4.0 → 7.5.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent/hooks/at_exit_hook.rb +16 -1
  3. data/lib/contrast/agent/middleware/middleware.rb +1 -1
  4. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +19 -12
  5. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +55 -20
  6. data/lib/contrast/agent/protect/policy/rule_applicator.rb +1 -4
  7. data/lib/contrast/agent/protect/rule/base.rb +56 -25
  8. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +12 -4
  9. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +0 -26
  10. data/lib/contrast/agent/protect/rule/cmdi/cmd_injection.rb +2 -5
  11. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +2 -4
  12. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +2 -1
  13. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +4 -4
  14. data/lib/contrast/agent/protect/rule/input_classification/base.rb +1 -4
  15. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +34 -2
  16. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli.rb +5 -2
  17. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal.rb +12 -7
  18. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +2 -2
  19. data/lib/contrast/agent/protect/rule/sqli/sqli_base_rule.rb +2 -3
  20. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +3 -4
  21. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload.rb +3 -0
  22. data/lib/contrast/agent/protect/rule/utils/builders.rb +3 -4
  23. data/lib/contrast/agent/protect/rule/utils/filters.rb +32 -16
  24. data/lib/contrast/agent/protect/rule/xss/xss.rb +80 -0
  25. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +9 -2
  26. data/lib/contrast/agent/reporting/details/xss_match.rb +17 -0
  27. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +32 -0
  28. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +4 -34
  29. data/lib/contrast/agent/reporting/reporting_events/finding.rb +1 -5
  30. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +2 -5
  31. data/lib/contrast/agent/reporting/reporting_utilities/build_preflight.rb +4 -4
  32. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +5 -1
  33. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +1 -1
  34. data/lib/contrast/agent/request/request_context_extend.rb +0 -2
  35. data/lib/contrast/agent/version.rb +1 -1
  36. data/lib/contrast/components/assess.rb +4 -0
  37. data/lib/contrast/framework/rails/support.rb +2 -2
  38. data/lib/contrast/logger/cef_log.rb +30 -4
  39. data/lib/contrast/utils/io_util.rb +3 -0
  40. data/lib/contrast/utils/json.rb +1 -1
  41. data/lib/contrast/utils/log_utils.rb +21 -10
  42. data/ruby-agent.gemspec +3 -2
  43. metadata +18 -12
@@ -37,7 +37,7 @@ module Contrast
37
37
  # The Base64 method will return printable ascii characters, so we can use this to determine if the value is
38
38
  # encoded or not.
39
39
  #
40
- # The solution in this case is encodind the value, and then decoding it. If the value is already encoded
40
+ # The solution in this case is encoding the value, and then decoding it. If the value is already encoded
41
41
  # it will not be eq to the original value. If the value is not encoded, it will be eq to the original value.
42
42
  #
43
43
  # @param value [String] input to check for encoding status.
@@ -96,11 +96,43 @@ module Contrast
96
96
  def cs__decode64 value, input_type
97
97
  return value unless cs__base64?(value, input_type)
98
98
 
99
- Base64.decode64(value)
99
+ new_value = try_base64_decode(value)
100
+ new_value, success = normalize_encoding(new_value)
101
+ return new_value if success
102
+
103
+ value
100
104
  rescue StandardError => e
101
105
  logger.error('Error while decoding base64', error: e, message: e.message, backtrace: e.backtrace)
102
106
  value
103
107
  end
108
+
109
+ private
110
+
111
+ # Try and decode the value, do not use decoding if the value have zero bytes.
112
+ def try_base64_decode value
113
+ new_value = Base64.decode64(value)
114
+ # check for null bytes:
115
+ return new_value unless new_value.bytes.select(&:zero?).any?
116
+
117
+ value
118
+ end
119
+
120
+ # Detecting encoded Base64 is not perfect. In some cases it will detect certain inputs as
121
+ # encoded and will try to decode them. Even if the decoding is successful, the value may be
122
+ # encoded back to ASCII format. AgentLib will raise UTF-8 error in this case.
123
+ # This method will try to normalize the encoding to UTF-8. If the encoding fails, this means
124
+ # that the decoding was not successful and the value will be returned as is. Otherwise a
125
+ # base64 decoded string with ASCII-8BIT encoding will be parsed to UTF-8 without errors.
126
+ #
127
+ # @param value [String] input to normalize.
128
+ # @return [Array<String, Boolean>] normalized value and success flag.
129
+ def normalize_encoding value
130
+ new_value = value.dup.encode!('Windows-1252').force_encoding('UTF-8')
131
+ [new_value, true]
132
+ rescue StandardError
133
+ # encoding failed, or the decoding from base64 failed.
134
+ [nil, false]
135
+ end
104
136
  end
105
137
  end
106
138
  end
@@ -32,6 +32,9 @@ module Contrast
32
32
  NAME
33
33
  end
34
34
 
35
+ # Return the specific blocking message for this rule.
36
+ #
37
+ # @return [String] the reason for the raised security exception.
35
38
  def block_message
36
39
  BLOCK_MESSAGE
37
40
  end
@@ -56,9 +59,9 @@ module Contrast
56
59
  return unless result
57
60
 
58
61
  append_to_activity(context, result)
59
-
62
+ record_triggered(context)
60
63
  cef_logging(result, :successful_attack)
61
- raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked?
64
+ raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked_violation?(result)
62
65
  end
63
66
 
64
67
  def build_attack_with_match context, input_analysis_result, result, candidate_string, **kwargs
@@ -26,6 +26,8 @@ module Contrast
26
26
  JSON_VALUE, MULTIPART_VALUE, MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE, URI
27
27
  ].cs__freeze
28
28
 
29
+ BLOCK_MESSAGE = 'Path Traversal rule triggered. Request blocked.'
30
+
29
31
  def rule_name
30
32
  NAME
31
33
  end
@@ -48,6 +50,13 @@ module Contrast
48
50
  APPLICABLE_USER_INPUTS
49
51
  end
50
52
 
53
+ # Return the specific blocking message for this rule.
54
+ #
55
+ # @return [String] the reason for the raised security exception.
56
+ def block_message
57
+ BLOCK_MESSAGE
58
+ end
59
+
51
60
  # Path Traversal input classification
52
61
  #
53
62
  # @return [module<Contrast::Agent::Protect::Rule::PathTraversalInputClassification>]
@@ -55,19 +64,15 @@ module Contrast
55
64
  @_classification ||= Contrast::Agent::Protect::Rule::PathTraversalInputClassification.cs__freeze
56
65
  end
57
66
 
58
- def infilter context, method, path
67
+ def infilter context, _method, path
59
68
  return unless infilter?(context)
60
69
 
61
70
  result = find_attacker(context, path)
62
71
  return unless result
63
72
 
64
73
  append_to_activity(context, result)
65
- return unless blocked?
66
-
67
- result_rule_name = Contrast::Utils::StringUtils.transform_string(result.rule_id)
68
- cef_logging(result, :successful_attack, value: path)
69
- exception_messasge = "#{ result_rule_name } rule triggered. Call to File.#{ method } blocked."
70
- raise(Contrast::SecurityException.new(self, exception_messasge))
74
+ record_triggered(context)
75
+ raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
71
76
  end
72
77
 
73
78
  protected
@@ -53,10 +53,10 @@ module Contrast
53
53
  return unless result
54
54
 
55
55
  append_to_activity(context, result)
56
- return unless blocked?
56
+ record_triggered(context)
57
+ return unless blocked_violation?(result)
57
58
 
58
59
  result_rule_name = Contrast::Utils::StringUtils.transform_string(result.rule_id)
59
- cef_logging(result, :successful_attack, value: path)
60
60
  exception_messasge = "#{ result_rule_name } rule triggered. Call to File.#{ method } blocked."
61
61
  raise(Contrast::SecurityException.new(self, exception_messasge))
62
62
  end
@@ -26,9 +26,8 @@ module Contrast
26
26
  return unless result
27
27
 
28
28
  append_to_activity(context, result)
29
-
30
- cef_logging(result, :successful_attack)
31
- raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked?
29
+ record_triggered(context)
30
+ raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked_violation?(result)
32
31
  end
33
32
  end
34
33
  end
@@ -26,9 +26,8 @@ module Contrast
26
26
  return unless result
27
27
 
28
28
  append_to_activity(context, result)
29
-
30
- cef_logging(result, :successful_attack)
31
- raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked?
29
+ record_triggered(context)
30
+ raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked_violation?(result)
32
31
  end
33
32
 
34
33
  protected
@@ -39,8 +38,8 @@ module Contrast
39
38
 
40
39
  def build_violation context, potential_attack_string
41
40
  result = build_attack_result(context)
42
- update_successful_attack_response(context, nil, result, potential_attack_string)
43
41
  append_sample(context, nil, result, potential_attack_string)
42
+ update_successful_attack_response(context, nil, result, potential_attack_string)
44
43
  result
45
44
  end
46
45
 
@@ -28,6 +28,9 @@ module Contrast
28
28
  APPLICABLE_USER_INPUTS
29
29
  end
30
30
 
31
+ # Return the specific blocking message for this rule.
32
+ #
33
+ # @return [String] the reason for the raised security exception.
31
34
  def block_message
32
35
  BLOCK_MESSAGE
33
36
  end
@@ -29,8 +29,8 @@ module Contrast
29
29
  # this input
30
30
  def build_attack_with_match context, ia_result, result, candidate_string, **kwargs
31
31
  result ||= build_attack_result(context)
32
- update_successful_attack_response(context, ia_result, result, candidate_string)
33
32
  append_sample(context, ia_result, result, candidate_string, **kwargs)
33
+ update_successful_attack_response(context, ia_result, result, candidate_string)
34
34
 
35
35
  result
36
36
  end
@@ -53,8 +53,8 @@ module Contrast
53
53
  # this input
54
54
  def build_attack_without_match context, ia_result, result, **kwargs
55
55
  result ||= build_attack_result(context)
56
- update_perimeter_attack_response(context, ia_result, result)
57
56
  append_sample(context, ia_result, result, nil, **kwargs)
57
+ update_perimeter_attack_response(context, ia_result, result)
58
58
 
59
59
  result
60
60
  end
@@ -97,11 +97,10 @@ module Contrast
97
97
  # @param potential_attack_string [String]
98
98
  def build_violation context, potential_attack_string
99
99
  result = build_attack_result(context)
100
+ append_sample(context, nil, result, potential_attack_string)
100
101
  update_successful_attack_response(context, nil, result, potential_attack_string)
101
102
  return unless result
102
103
 
103
- append_sample(context, nil, result, potential_attack_string)
104
- cef_logging(result, :successful_attack)
105
104
  result
106
105
  end
107
106
  end
@@ -25,11 +25,12 @@ module Contrast
25
25
 
26
26
  ia_results.each do |ia_result|
27
27
  result = build_attack_result(context)
28
- build_attack_without_match(context, ia_result, result)
29
- append_to_activity(context, result)
28
+ result = build_attack_without_match(context, ia_result, result)
29
+ next unless result
30
30
 
31
- cef_logging(result, :successful_attack)
32
- raise(Contrast::SecurityException.new(self, block_message)) if blocked?
31
+ append_to_activity(context, result)
32
+ record_triggered(context)
33
+ raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
33
34
  end
34
35
  end
35
36
 
@@ -39,10 +40,10 @@ module Contrast
39
40
  # @param context [Contrast::Agent::RequestContext]
40
41
  # @return [Boolean]
41
42
  def prefilter? context
42
- return false unless context
43
43
  return false unless enabled?
44
- return false unless (results = gather_ia_results(context)) && results.any?
45
44
  return false if protect_excluded_by_url?(rule_name)
45
+ return false unless context
46
+ return false unless (results = gather_ia_results(context)) && results.any?
46
47
  return false if protect_excluded_by_input?(results)
47
48
 
48
49
  true
@@ -52,13 +53,20 @@ module Contrast
52
53
  # have a different implementation based on the rule. As such, there
53
54
  # is not parent implementation.
54
55
  #
55
- # @param _context [Contrast::Agent::RequestContext] the context for
56
+ # @param context [Contrast::Agent::RequestContext] the context for
56
57
  # the current request
57
- # @param _match_string [String] the input that violated the rule and
58
+ # @param match_string [String] the input that violated the rule and
58
59
  # matched the attack detection logic
59
60
  # @param _kwargs [Hash] key-value pairs used by the rule to build a
60
61
  # report.
61
- def infilter _context, _match_string, **_kwargs; end
62
+ def infilter context, match_string, _kwargs
63
+ return unless infilter?(context)
64
+ return unless (result = build_violation(context, match_string))
65
+
66
+ append_to_activity(context, result)
67
+ record_triggered(context)
68
+ raise(Contrast::SecurityException.new(self, block_message)) if blocked?
69
+ end
62
70
 
63
71
  # Infilter check always called before infilter to check if the rule is infilter
64
72
  # capable, not disabled or in other way excluded by url or input exclusions.
@@ -74,6 +82,18 @@ module Contrast
74
82
  true
75
83
  end
76
84
 
85
+ # Check befor commiting infilter
86
+ #
87
+ # @param context [Contrast::Agent::RequestContext]
88
+ def postfilter? context
89
+ return false unless enabled? && POSTFILTER_MODES.include?(mode)
90
+ return false if protect_excluded_by_url?(rule_name)
91
+ return false if protect_excluded_by_input?(gather_ia_results(context))
92
+ return false if mode == :NO_ACTION || mode == :PERMIT
93
+
94
+ true
95
+ end
96
+
77
97
  # Actions required for the rules that have to happen after the
78
98
  # application has completed its processing of the request.
79
99
  #
@@ -88,18 +108,14 @@ module Contrast
88
108
  # @param context [Contrast::Agent::RequestContext]
89
109
  # @raise [Contrast::SecurityException]
90
110
  def postfilter context
91
- return unless enabled? && POSTFILTER_MODES.include?(mode)
92
- return false if protect_excluded_by_url?(rule_name)
93
- return if protect_excluded_by_input?(gather_ia_results(context))
94
-
95
- return if mode == :NO_ACTION || mode == :PERMIT
111
+ return unless postfilter?(context)
96
112
 
97
113
  result = find_postfilter_attacker(context, nil)
98
114
  return unless result&.samples&.any?
99
115
 
100
- cef_logging(result)
101
116
  append_to_activity(context, result)
102
- return unless result.response == :BLOCKED
117
+ record_triggered(context)
118
+ return unless blocked_violation?(result)
103
119
 
104
120
  raise(Contrast::SecurityException.new(self, "#{ rule_name } triggered in postfilter. Response blocked."))
105
121
  end
@@ -3,6 +3,8 @@
3
3
 
4
4
  require 'contrast/agent/protect/rule/base'
5
5
  require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
6
+ require 'contrast/agent/reporting/details/xss_details'
7
+ require 'contrast/agent/reporting/details/xss_match'
6
8
  require 'contrast/agent/reporting/input_analysis/input_type'
7
9
 
8
10
  module Contrast
@@ -25,10 +27,56 @@ module Contrast
25
27
  NAME
26
28
  end
27
29
 
30
+ # Return the specific blocking message for this rule.
31
+ #
32
+ # @return [String] the reason for the raised security exception.
28
33
  def block_message
29
34
  BLOCK_MESSAGE
30
35
  end
31
36
 
37
+ # Prefilter check always called before infilter to check if the rule is infilter
38
+ # capable, not disabled or in other way excluded by url or input exclusions.
39
+ #
40
+ # @param context [Contrast::Agent::RequestContext]
41
+ # @return [Boolean]
42
+ def prefilter? context
43
+ return false unless enabled?
44
+ return false if protect_excluded_by_url?(rule_name)
45
+ return false unless context
46
+ return false unless (results = gather_ia_results(context)) && results.any?
47
+ return false if protect_excluded_by_input?(results)
48
+
49
+ true
50
+ end
51
+
52
+ def prefilter context
53
+ return unless prefilter?(context)
54
+
55
+ ia_results = gather_ia_results(context)
56
+
57
+ ia_results.each do |ia_result|
58
+ result = build_attack_result(context)
59
+ result = build_attack_without_match(context, ia_result, result)
60
+ next unless result
61
+
62
+ append_to_activity(context, result)
63
+ # XSS is being triggered, so we need to add it to the triggered rules,
64
+ # So the IA won't be done for this rule again for the current request.
65
+ record_triggered(context)
66
+ raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
67
+ end
68
+ end
69
+
70
+ # XSS is evaluated only on prefilter
71
+ def infilter? _context
72
+ false
73
+ end
74
+
75
+ # XSS is evaluated only on prefilter
76
+ def postfilter? _context
77
+ false
78
+ end
79
+
32
80
  # XSS Upload input classification
33
81
  #
34
82
  # @return [module<Contrast::Agent::Protect::Rule::ReflectedXssInputClassification>]
@@ -43,6 +91,38 @@ module Contrast
43
91
  def applicable_user_inputs
44
92
  APPLICABLE_USER_INPUTS
45
93
  end
94
+
95
+ # Adding XSS details
96
+ #
97
+ # @param context [Contrast::Agent::RequestContext]
98
+ # @param ia_result [Contrast::Agent::Reporting::InputAnalysisResult]
99
+ # @param _xss_string
100
+ # @param **_kwargs
101
+ # @return [Contrast::Agent::Reporting::RaspRuleSample]
102
+ def build_sample context, ia_result, _xss_string, **_kwargs
103
+ sample = build_base_sample(context, ia_result)
104
+ sample.details = Contrast::Agent::Reporting::Details::XssDetails.new
105
+ sample.details.input = ia_result.value
106
+
107
+ # TODO: RUBY-99999 check the if the ReflectedXss matches are needed.
108
+ xss_match = Contrast::Agent::Reporting::Details::XssMatch.new(ia_result.value)
109
+ sample.details.matches << xss_match unless xss_match.empty?
110
+
111
+ sample
112
+ end
113
+
114
+ private
115
+
116
+ # @param context [Contrast::Agent::RequestContext]
117
+ # @param potential_attack_string [String, nil]
118
+ # @return [Contrast::Agent::Reporting::AttackResult, nil]
119
+ def find_postfilter_attacker context, potential_attack_string, **kwargs
120
+ ia_results = gather_ia_results(context)
121
+ ia_results.reject! do |ia_result|
122
+ ia_result.score_level == Contrast::Agent::Reporting::ScoreLevel::IGNORE
123
+ end
124
+ find_attacker_with_results(context, potential_attack_string, ia_results, **kwargs)
125
+ end
46
126
  end
47
127
  end
48
128
  end
@@ -26,6 +26,13 @@ module Contrast
26
26
  NAME
27
27
  end
28
28
 
29
+ # Return the specific blocking message for this rule.
30
+ #
31
+ # @return [String] the reason for the raised security exception.
32
+ def block_message
33
+ BLOCK_MESSAGE
34
+ end
35
+
29
36
  # Given an xml, evaluate it for an XXE attack. There's no return here
30
37
  # as this method handles appending the evaluation to the request
31
38
  # context, connecting it to the reporting mechanism at request end.
@@ -43,9 +50,9 @@ module Contrast
43
50
  return unless result
44
51
 
45
52
  append_to_activity(context, result)
46
- return unless blocked?
53
+ record_triggered(context)
54
+ return unless blocked_violation?(result)
47
55
 
48
- cef_logging(result, :successful_attack, value: xml)
49
56
  raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE))
50
57
  end
51
58
 
@@ -9,6 +9,9 @@ module Contrast
9
9
  module Details
10
10
  # Matcher data for XSS rule.
11
11
  class XssMatch
12
+ EVIDENCE_START = /<script.*?>/i.cs__freeze
13
+ EVIDENCE_END = %r{</script.*?>}i.cs__freeze
14
+
12
15
  # @return [Integer] in ms
13
16
  attr_accessor :evidence_start
14
17
  # @return [String]
@@ -16,6 +19,16 @@ module Contrast
16
19
  # @return [Integer]
17
20
  attr_accessor :offset
18
21
 
22
+ # @param xss_string [String] to check for matches.
23
+ def initialize xss_string = ''
24
+ return if xss_string.empty?
25
+
26
+ @evidence_start = xss_string.index(EVIDENCE_START)
27
+ @offset = (xss_string[0...@evidence_start] || '').length
28
+ @evidence = xss_string[@evidence_start...xss_string.index(EVIDENCE_END)].gsub(EVIDENCE_START, '').
29
+ gsub(EVIDENCE_END, '')
30
+ end
31
+
19
32
  def to_controlled_hash
20
33
  {
21
34
  evidenceStart: evidence_start,
@@ -23,6 +36,10 @@ module Contrast
23
36
  offset: offset
24
37
  }
25
38
  end
39
+
40
+ def empty?
41
+ evidence_start.nil? || evidence.nil? || offset.nil?
42
+ end
26
43
  end
27
44
  end
28
45
  end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/utils/object_share'
5
+ require 'contrast/utils/duck_utils'
5
6
  require 'contrast/agent/reporting/input_analysis/input_analysis_result'
6
7
 
7
8
  module Contrast
@@ -12,10 +13,20 @@ module Contrast
12
13
  # @return [Hash] Stored request inputs for this context.
13
14
  attr_accessor :inputs
14
15
 
16
+ # Records rules triggered on sink.
17
+ #
18
+ # @return [Array<String>] array with rule names.
15
19
  def triggered_rules
16
20
  @_triggered_rules ||= []
17
21
  end
18
22
 
23
+ # Records rules with input analysis for current request.
24
+ #
25
+ # @return [Array<String>] array with rule names.
26
+ def analysed_rules
27
+ @_analysed_rules ||= []
28
+ end
29
+
19
30
  # result from input analysis
20
31
  #
21
32
  # @return @_results [Array<Contrast::Agent::Reporting::Settings::InputAnalysisResult>]
@@ -44,6 +55,27 @@ module Contrast
44
55
  def request= request
45
56
  @_request = request if request.instance_of?(Contrast::Agent::Request)
46
57
  end
58
+
59
+ # Check if the InputAnalysis is empty.
60
+ def no_inputs?
61
+ Contrast::Utils::DuckUtils.empty_duck?(inputs)
62
+ end
63
+
64
+ # We add the analysed rules to the list. The IA won't be build for this rule,
65
+ # since already it's already available.
66
+ #
67
+ # @param rule_name [String] name to record.
68
+ def record_analysed_rule rule_name
69
+ analysed_rules << rule_name unless triggered_rules.include?(rule_name)
70
+ end
71
+
72
+ # We add the triggered rules to the list. After request analysis will skip this rule
73
+ # as already triggered.
74
+ #
75
+ # @param rule_name [String] name to record.
76
+ def record_rule_triggered rule_name
77
+ triggered_rules << rule_name unless triggered_rules.include?(rule_name)
78
+ end
47
79
  end
48
80
  end
49
81
  end
@@ -33,42 +33,12 @@ module Contrast
33
33
  # @return
34
34
  def to_a
35
35
  @_to_a ||= [
36
- UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME, PARAMETER_VALUE,
37
- QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE, MULTIPART_VALUE,
38
- MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST, URL_PARAMETER, UNKNOWN
36
+ UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME,
37
+ PARAMETER_VALUE, QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE,
38
+ MULTIPART_VALUE, MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST,
39
+ URL_PARAMETER, UNKNOWN
39
40
  ]
40
41
  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
72
42
  end
73
43
  end
74
44
  end
@@ -119,16 +119,12 @@ module Contrast
119
119
  ruleId: rule_id,
120
120
  session_id: ::Contrast::ASSESS.session_id,
121
121
  version: 4
122
- }
122
+ }.compact
123
123
  end
124
124
 
125
125
  # @raise [ArgumentError]
126
126
  def validate
127
127
  raise(ArgumentError, "#{ self } did not have a proper rule. Unable to continue.") unless @rule_id
128
-
129
- unless ::Contrast::ASSESS.session_id
130
- raise(ArgumentError, "#{ self } did not have a proper session id. Unable to continue.")
131
- end
132
128
  if event_based? && events.empty?
133
129
  raise(ArgumentError, "#{ self } did not have proper events for #{ @rule_id }. Unable to continue.")
134
130
  end
@@ -43,11 +43,11 @@ module Contrast
43
43
  appPath: ::Contrast::APP_CONTEXT.name, # rubocop:disable Security/Module/Name
44
44
  appVersion: ::Contrast::APP_CONTEXT.version,
45
45
  code: CODE,
46
- data: '',
46
+ data: @data || '',
47
47
  key: 0,
48
48
  session_id: ::Contrast::ASSESS.session_id,
49
49
  routes: @routes.map(&:to_controlled_hash)
50
- }
50
+ }.compact
51
51
  end
52
52
 
53
53
  # @raise [ArgumentError]
@@ -55,9 +55,6 @@ module Contrast
55
55
  unless Contrast::Utils::StringUtils.present?(data)
56
56
  raise(ArgumentError, "#{ cs__class } did not have a proper data. Unable to continue.")
57
57
  end
58
- unless ::Contrast::ASSESS.session_id
59
- raise(ArgumentError, "#{ cs__class } did not have a proper session id. Unable to continue.")
60
- end
61
58
  unless Contrast::APP_CONTEXT.name # rubocop:disable Security/Module/Name
62
59
  raise(ArgumentError, "#{ cs__class } did not have a proper Application Name. Unable to continue.")
63
60
  end
@@ -17,14 +17,14 @@ module Contrast
17
17
  # @param finding [Contrast::Agent::Reporting::Finding]
18
18
  # @return [Contrast::Agent::Reporting::Preflight, nil]
19
19
  def generate finding
20
- return unless finding
20
+ return unless finding&.cs__is_a?(Contrast::Agent::Reporting::Finding)
21
21
 
22
22
  new_preflight = Contrast::Agent::Reporting::Preflight.new
23
23
  new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new
24
- finding.routes.each do |route|
25
- new_preflight_message.routes << route
24
+ routes = finding.routes
25
+ unless Contrast::Utils::DuckUtils.empty_duck?(routes)
26
+ routes.each { |route| new_preflight_message.routes << route }
26
27
  end
27
- new_preflight_message.hash_code = finding.hash_code
28
28
  new_preflight_message.data = "#{ finding.rule_id },#{ finding.hash_code }"
29
29
  new_preflight.messages << new_preflight_message
30
30
  return new_preflight unless Contrast::Utils::DuckUtils.empty_duck?(new_preflight.messages)
@@ -118,7 +118,11 @@ 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
+ preflight_data = preflight_message&.data
122
+ corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_data)
123
+ if Contrast::Agent::REQUEST_TRACKER.current
124
+ Contrast::Agent::REQUEST_TRACKER.current.reported_findings << preflight_data
125
+ end
122
126
  next unless corresponding_finding
123
127
 
124
128
  send_event(corresponding_finding, connection)
@@ -263,7 +263,7 @@ module Contrast
263
263
  extract_response_last_modified(response, event)
264
264
  populate_response(response_data, event)
265
265
  rescue StandardError => e
266
- logger.error('Unable to convert response', e)
266
+ logger.error('Unable to convert response', error: e)
267
267
  nil
268
268
  end
269
269