contrast-agent 7.3.2 → 7.4.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent/middleware/middleware.rb +1 -1
  3. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +9 -11
  4. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +55 -20
  5. data/lib/contrast/agent/protect/policy/rule_applicator.rb +1 -4
  6. data/lib/contrast/agent/protect/rule/base.rb +61 -26
  7. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +12 -4
  8. data/lib/contrast/agent/protect/rule/cmdi/cmd_injection.rb +19 -15
  9. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +2 -4
  10. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +2 -1
  11. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +4 -4
  12. data/lib/contrast/agent/protect/rule/input_classification/base.rb +7 -2
  13. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +1 -1
  14. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli.rb +5 -2
  15. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal.rb +20 -8
  16. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +2 -2
  17. data/lib/contrast/agent/protect/rule/sqli/sqli.rb +8 -1
  18. data/lib/contrast/agent/protect/rule/sqli/sqli_base_rule.rb +2 -3
  19. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +3 -4
  20. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload.rb +3 -0
  21. data/lib/contrast/agent/protect/rule/utils/builders.rb +3 -4
  22. data/lib/contrast/agent/protect/rule/utils/filters.rb +32 -16
  23. data/lib/contrast/agent/protect/rule/xss/xss.rb +80 -0
  24. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +9 -2
  25. data/lib/contrast/agent/protect/state.rb +110 -0
  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/reporting_events/application_defend_attack_sample_activity.rb +2 -0
  29. data/lib/contrast/agent/reporting/reporting_events/architecture_component.rb +2 -0
  30. data/lib/contrast/agent/reporting/reporting_events/finding.rb +1 -4
  31. data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +4 -0
  32. data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +2 -0
  33. data/lib/contrast/agent/reporting/reporting_events/observed_library_usage.rb +2 -0
  34. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +9 -8
  35. data/lib/contrast/agent/reporting/reporting_events/reportable_hash.rb +30 -6
  36. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +1 -1
  37. data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +1 -1
  38. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +1 -5
  39. data/lib/contrast/agent/reporting/settings/protect.rb +3 -3
  40. data/lib/contrast/agent/reporting/settings/sampling.rb +5 -4
  41. data/lib/contrast/agent/request/request_context_extend.rb +0 -2
  42. data/lib/contrast/agent/version.rb +1 -1
  43. data/lib/contrast/components/agent.rb +3 -5
  44. data/lib/contrast/components/api.rb +3 -3
  45. data/lib/contrast/components/assess.rb +4 -0
  46. data/lib/contrast/components/assess_rules.rb +1 -2
  47. data/lib/contrast/components/base.rb +1 -2
  48. data/lib/contrast/components/config/sources.rb +23 -0
  49. data/lib/contrast/components/logger.rb +19 -0
  50. data/lib/contrast/components/protect.rb +55 -14
  51. data/lib/contrast/components/sampling.rb +5 -12
  52. data/lib/contrast/components/security_logger.rb +17 -0
  53. data/lib/contrast/components/settings.rb +110 -76
  54. data/lib/contrast/config/certification_configuration.rb +1 -1
  55. data/lib/contrast/config/configuration_files.rb +0 -2
  56. data/lib/contrast/config/diagnostics/config.rb +3 -3
  57. data/lib/contrast/config/diagnostics/effective_config.rb +1 -1
  58. data/lib/contrast/config/diagnostics/environment_variables.rb +21 -11
  59. data/lib/contrast/config/diagnostics/monitor.rb +1 -1
  60. data/lib/contrast/config/diagnostics/singleton_tools.rb +170 -0
  61. data/lib/contrast/config/diagnostics/source_config_value.rb +14 -9
  62. data/lib/contrast/config/diagnostics/tools.rb +23 -84
  63. data/lib/contrast/config/request_audit_configuration.rb +1 -1
  64. data/lib/contrast/config/server_configuration.rb +3 -15
  65. data/lib/contrast/configuration.rb +5 -2
  66. data/lib/contrast/framework/manager.rb +4 -3
  67. data/lib/contrast/framework/manager_extend.rb +3 -1
  68. data/lib/contrast/framework/rack/support.rb +11 -2
  69. data/lib/contrast/framework/rails/support.rb +2 -2
  70. data/lib/contrast/logger/cef_log.rb +30 -4
  71. data/lib/contrast/utils/io_util.rb +3 -0
  72. data/lib/contrast/utils/log_utils.rb +22 -11
  73. data/lib/contrast/utils/request_utils.rb +1 -1
  74. data/lib/contrast/utils/timer.rb +1 -1
  75. metadata +4 -2
@@ -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,21 +26,37 @@ 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
32
34
 
35
+ # Sub-rules forwarders:
36
+
37
+ # @return [Contrast::Agent::Protect::Rule::PathTraversalSemanticBypass]
38
+ def semantic_file_security_bypass
39
+ @_semantic_file_security_bypass ||= Contrast::Agent::Protect::Rule::PathTraversalSemanticBypass.new
40
+ end
41
+
33
42
  # Array of sub_rules
34
43
  #
35
44
  # @return [Array]
36
45
  def sub_rules
37
- @_sub_rules ||= [Contrast::Agent::Protect::Rule::PathTraversalSemanticBypass.new].cs__freeze
46
+ @_sub_rules ||= [semantic_file_security_bypass].cs__freeze
38
47
  end
39
48
 
40
49
  def applicable_user_inputs
41
50
  APPLICABLE_USER_INPUTS
42
51
  end
43
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
+
44
60
  # Path Traversal input classification
45
61
  #
46
62
  # @return [module<Contrast::Agent::Protect::Rule::PathTraversalInputClassification>]
@@ -48,19 +64,15 @@ module Contrast
48
64
  @_classification ||= Contrast::Agent::Protect::Rule::PathTraversalInputClassification.cs__freeze
49
65
  end
50
66
 
51
- def infilter context, method, path
67
+ def infilter context, _method, path
52
68
  return unless infilter?(context)
53
69
 
54
70
  result = find_attacker(context, path)
55
71
  return unless result
56
72
 
57
73
  append_to_activity(context, result)
58
- return unless blocked?
59
-
60
- result_rule_name = Contrast::Utils::StringUtils.transform_string(result.rule_id)
61
- cef_logging(result, :successful_attack, value: path)
62
- exception_messasge = "#{ result_rule_name } rule triggered. Call to File.#{ method } blocked."
63
- raise(Contrast::SecurityException.new(self, exception_messasge))
74
+ record_triggered(context)
75
+ raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
64
76
  end
65
77
 
66
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
@@ -35,11 +35,18 @@ module Contrast
35
35
  BLOCK_MESSAGE
36
36
  end
37
37
 
38
+ # Sub-rules forwarders:
39
+
40
+ # @return [Contrast::Agent::Protect::Rule::SqliDangerousFunctions]
41
+ def semantic_dangerous_functions
42
+ @_semantic_dangerous_functions ||= Contrast::Agent::Protect::Rule::SqliDangerousFunctions.new
43
+ end
44
+
38
45
  # Array of sub_rules
39
46
  #
40
47
  # @return [Array]
41
48
  def sub_rules
42
- @_sub_rules ||= [Contrast::Agent::Protect::Rule::SqliDangerousFunctions.new].cs__freeze
49
+ @_sub_rules ||= [semantic_dangerous_functions].cs__freeze
43
50
  end
44
51
 
45
52
  def applicable_user_inputs
@@ -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
 
@@ -0,0 +1,110 @@
1
+ # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/utils/object_share'
5
+ require 'contrast/components/logger'
6
+
7
+ module Contrast
8
+ module Agent
9
+ module Protect
10
+ # Master class for each protect rule. This class will hold all the rules references.
11
+ # Any access to the rules should be done through this class. and new rules should be
12
+ # added here. Each main rule should require and include and initialize it's sub-rules.
13
+ class State
14
+ include Contrast::Components::Logger::InstanceMethods
15
+
16
+ # @return [boolean] State dictated by local or server settings
17
+ attr_accessor :enabled
18
+ # @return [Contrast::Agent::Protect::Rule::BotBlocker] the bot blocker rule
19
+ attr_reader :bot_blocker
20
+ # @return [Contrast::Agent::Protect::Rule::CmdInjection] the command injection rule
21
+ attr_reader :cmd_injection
22
+ # @return [Contrast::Agent::Protect::Rule::CmdiBackdoors]
23
+ attr_reader :cmd_injection_command_backdoors
24
+ # @return [Contrast::Agent::Protect::Rule::CmdiChainedCommand]
25
+ attr_reader :cmd_injection_semantic_chained_commands
26
+ # @return [Contrast::Agent::Protect::Rule::CmdiDangerousPath]
27
+ attr_reader :cmd_injection_semantic_dangerous_paths
28
+ # @return [Contrast::Agent::Protect::Rule::Deserialization]
29
+ attr_reader :untrusted_deserialization
30
+ # @return [Contrast::Agent::Protect::Rule::NoSqli]
31
+ attr_reader :nosql_injection
32
+ # @return [Contrast::Agent::Protect::Rule::PathTraversal]
33
+ attr_reader :path_traversal
34
+ # @return [Contrast::Agent::Protect::Rule::PathTraversalSemanticBypass]
35
+ attr_reader :path_traversal_semantic_file_security_bypass
36
+ # @return [Contrast::Agent::Protect::Rule::Sqli]
37
+ attr_reader :sql_injection
38
+ # @return [Contrast::Agent::Protect::Rule::SqliDangerousFunctions]
39
+ attr_reader :sql_injection_semantic_dangerous_functions
40
+ # @return [Contrast::Agent::Protect::Rule::UnsafeFileUpload] the unsafe file upload rule
41
+ attr_reader :unsafe_file_upload
42
+ # @return [Contrast::Agent::Protect::Rule::Xss] the reflected xss rule
43
+ attr_reader :reflected_xss
44
+ # @return [Contrast::Agent::Protect::Rule::Xxe] the xxe rule
45
+ attr_reader :xxe
46
+
47
+ # Initialize all the protect rules. This should be the one place to access each live
48
+ # rule reference.
49
+ def initialize
50
+ @bot_blocker = Contrast::Agent::Protect::Rule::BotBlocker.new
51
+ @cmd_injection = Contrast::Agent::Protect::Rule::CmdInjection.new
52
+ @cmd_injection_command_backdoors = @cmd_injection.command_backdoors
53
+ @cmd_injection_semantic_chained_commands = @cmd_injection.semantic_chained_commands
54
+ @cmd_injection_semantic_dangerous_paths = @cmd_injection.semantic_dangerous_paths
55
+ @untrusted_deserialization = Contrast::Agent::Protect::Rule::Deserialization.new
56
+ @nosql_injection = Contrast::Agent::Protect::Rule::NoSqli.new
57
+ @path_traversal = Contrast::Agent::Protect::Rule::PathTraversal.new
58
+ @path_traversal_semantic_file_security_bypass = @path_traversal.semantic_file_security_bypass
59
+ @sql_injection = Contrast::Agent::Protect::Rule::Sqli.new
60
+ @sql_injection_semantic_dangerous_functions = @sql_injection.semantic_dangerous_functions
61
+ @unsafe_file_upload = Contrast::Agent::Protect::Rule::UnsafeFileUpload.new
62
+ @reflected_xss = Contrast::Agent::Protect::Rule::Xss.new
63
+ @xxe = Contrast::Agent::Protect::Rule::Xxe.new
64
+ end
65
+
66
+ # Return the Rules in Hash form {rule_id => rule_class }.
67
+ # This is used to traverse for each rule and update it's settings.
68
+ # Also is the way a rule is retrieved given the ID is known.
69
+ #
70
+ # @return [Hash<String, Contrast::Agent::Protect::Rule::Base>]
71
+ def rules
72
+ @_rules ||= {
73
+ @bot_blocker.rule_name => @bot_blocker,
74
+ @cmd_injection.rule_name => @cmd_injection,
75
+ @cmd_injection_command_backdoors.rule_name => @cmd_injection_command_backdoors,
76
+ @cmd_injection_semantic_chained_commands.rule_name => @cmd_injection_semantic_chained_commands,
77
+ @cmd_injection_semantic_dangerous_paths.rule_name => @cmd_injection_semantic_dangerous_paths,
78
+ @untrusted_deserialization.rule_name => @untrusted_deserialization,
79
+ @nosql_injection.rule_name => @nosql_injection,
80
+ @path_traversal.rule_name => @path_traversal,
81
+ @path_traversal_semantic_file_security_bypass.rule_name => @path_traversal_semantic_file_security_bypass,
82
+ @sql_injection.rule_name => @sql_injection,
83
+ @sql_injection_semantic_dangerous_functions.rule_name => @sql_injection_semantic_dangerous_functions,
84
+ @unsafe_file_upload.rule_name => @unsafe_file_upload,
85
+ @reflected_xss.rule_name => @reflected_xss,
86
+ @xxe.rule_name => @xxe
87
+ }
88
+ end
89
+
90
+ # Update all settings from configuration.
91
+ def update
92
+ rules.values.each(&:update)
93
+ logger.info('Current rule settings:')
94
+ rules.each { |k, v| logger.info('Protect Rule mode set', rule: k, mode: v.mode) }
95
+ end
96
+
97
+ # Check the local configurations first then the server settings.
98
+ def enabled?
99
+ Contrast::PROTECT.enable || Contrast::SETTINGS.protect_state.enabled
100
+ end
101
+
102
+ # @param [String] rule_id
103
+ # @return [Contrast::Agent::Protect::Rule::Base]
104
+ def [] rule_id
105
+ rules[rule_id]
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -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
@@ -32,6 +32,8 @@ module Contrast
32
32
 
33
33
  def validate
34
34
  raise(ArgumentError, 'Start Time is not presented') unless @start_time
35
+
36
+ nil
35
37
  end
36
38
 
37
39
  # @param attack_result [Contrast::Agent::Reporting::AttackResult]
@@ -59,6 +59,8 @@ module Contrast
59
59
  raise(ArgumentError, "#{ self } did not have a proper type - '#{ type }'. Unable to continue.")
60
60
  end
61
61
  raise(ArgumentError, "#{ self } did not have a proper URL. Unable to continue.") unless url
62
+
63
+ nil
62
64
  end
63
65
  end
64
66
  end
@@ -119,15 +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
- unless ::Contrast::ASSESS.session_id
129
- raise(ArgumentError, "#{ self } did not have a proper session id. Unable to continue.")
130
- end
131
128
  if event_based? && events.empty?
132
129
  raise(ArgumentError, "#{ self } did not have proper events for #{ @rule_id }. Unable to continue.")
133
130
  end