contrast-agent 5.1.0 → 5.2.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cs__assess_kernel/cs__assess_kernel.c +7 -4
  3. data/ext/cs__assess_module/cs__assess_module.c +7 -7
  4. data/ext/cs__common/cs__common.c +4 -0
  5. data/ext/cs__common/cs__common.h +1 -0
  6. data/ext/cs__contrast_patch/cs__contrast_patch.c +52 -27
  7. data/ext/cs__contrast_patch/cs__contrast_patch.h +2 -0
  8. data/ext/cs__scope/cs__scope.c +747 -0
  9. data/ext/cs__scope/cs__scope.h +88 -0
  10. data/ext/cs__scope/extconf.rb +5 -0
  11. data/lib/contrast/agent/assess/contrast_event.rb +20 -13
  12. data/lib/contrast/agent/assess/contrast_object.rb +4 -1
  13. data/lib/contrast/agent/assess/policy/propagation_node.rb +2 -5
  14. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +2 -0
  15. data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -1
  16. data/lib/contrast/agent/assess/rule/response/{autocomplete_rule.rb → auto_complete_rule.rb} +4 -3
  17. data/lib/contrast/agent/assess/rule/response/base_rule.rb +12 -79
  18. data/lib/contrast/agent/assess/rule/response/body_rule.rb +109 -0
  19. data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +157 -0
  20. data/lib/contrast/agent/assess/rule/response/click_jacking_header_rule.rb +26 -0
  21. data/lib/contrast/agent/assess/rule/response/csp_header_insecure_rule.rb +14 -15
  22. data/lib/contrast/agent/assess/rule/response/csp_header_missing_rule.rb +5 -25
  23. data/lib/contrast/agent/assess/rule/response/framework/rails_support.rb +29 -0
  24. data/lib/contrast/agent/assess/rule/response/header_rule.rb +70 -0
  25. data/lib/contrast/agent/assess/rule/response/hsts_header_rule.rb +12 -36
  26. data/lib/contrast/agent/assess/rule/response/parameters_pollution_rule.rb +2 -1
  27. data/lib/contrast/agent/assess/rule/response/x_content_type_header_rule.rb +26 -0
  28. data/lib/contrast/agent/assess/rule/response/x_xss_protection_header_rule.rb +36 -0
  29. data/lib/contrast/agent/middleware.rb +1 -0
  30. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +1 -3
  31. data/lib/contrast/agent/patching/policy/patch.rb +2 -6
  32. data/lib/contrast/agent/patching/policy/patcher.rb +1 -1
  33. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +94 -0
  34. data/lib/contrast/agent/protect/rule/base.rb +28 -1
  35. data/lib/contrast/agent/protect/rule/base_service.rb +10 -1
  36. data/lib/contrast/agent/protect/rule/cmd_injection.rb +2 -0
  37. data/lib/contrast/agent/protect/rule/deserialization.rb +6 -0
  38. data/lib/contrast/agent/protect/rule/http_method_tampering.rb +5 -1
  39. data/lib/contrast/agent/protect/rule/no_sqli.rb +1 -0
  40. data/lib/contrast/agent/protect/rule/path_traversal.rb +1 -0
  41. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +124 -0
  42. data/lib/contrast/agent/protect/rule/sqli/sqli_worth_watching.rb +121 -0
  43. data/lib/contrast/agent/protect/rule/sqli.rb +33 -0
  44. data/lib/contrast/agent/protect/rule/xxe.rb +4 -0
  45. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +44 -0
  46. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +115 -0
  47. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +44 -0
  48. data/lib/contrast/agent/reporting/input_analysis/score_level.rb +21 -0
  49. data/lib/contrast/agent/reporting/report.rb +1 -0
  50. data/lib/contrast/agent/reporting/reporter.rb +8 -1
  51. data/lib/contrast/agent/reporting/reporting_events/finding.rb +69 -36
  52. data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +88 -59
  53. data/lib/contrast/agent/reporting/reporting_events/{finding_object.rb → finding_event_object.rb} +24 -20
  54. data/lib/contrast/agent/reporting/reporting_events/finding_event_parent_object.rb +39 -0
  55. data/lib/contrast/agent/reporting/reporting_events/finding_event_property.rb +40 -0
  56. data/lib/contrast/agent/reporting/reporting_events/{finding_signature.rb → finding_event_signature.rb} +29 -24
  57. data/lib/contrast/agent/reporting/reporting_events/finding_event_source.rb +12 -8
  58. data/lib/contrast/agent/reporting/reporting_events/{finding_stack.rb → finding_event_stack.rb} +23 -19
  59. data/lib/contrast/agent/reporting/reporting_events/{finding_taint_range.rb → finding_event_taint_range.rb} +17 -15
  60. data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +26 -53
  61. data/lib/contrast/agent/reporting/reporting_events/poll.rb +29 -0
  62. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +5 -4
  63. data/lib/contrast/agent/reporting/reporting_events/route_discovery.rb +1 -0
  64. data/lib/contrast/agent/reporting/reporting_events/server_activity.rb +1 -1
  65. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
  66. data/lib/contrast/agent/reporting/reporting_utilities/endpoints.rb +0 -1
  67. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +1 -0
  68. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +28 -20
  69. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +1 -1
  70. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +13 -1
  71. data/lib/contrast/agent/request_context.rb +6 -1
  72. data/lib/contrast/agent/request_context_extend.rb +85 -21
  73. data/lib/contrast/agent/scope.rb +102 -107
  74. data/lib/contrast/agent/service_heartbeat.rb +45 -2
  75. data/lib/contrast/agent/version.rb +1 -1
  76. data/lib/contrast/api/decorators/bot_blocker.rb +37 -0
  77. data/lib/contrast/api/decorators/ip_denylist.rb +37 -0
  78. data/lib/contrast/api/decorators/rasp_rule_sample.rb +29 -0
  79. data/lib/contrast/api/decorators/user_input.rb +11 -1
  80. data/lib/contrast/api/decorators/virtual_patch.rb +34 -0
  81. data/lib/contrast/components/logger.rb +5 -0
  82. data/lib/contrast/components/protect.rb +4 -2
  83. data/lib/contrast/components/scope.rb +98 -91
  84. data/lib/contrast/config/agent_configuration.rb +58 -12
  85. data/lib/contrast/config/api_configuration.rb +100 -12
  86. data/lib/contrast/config/api_proxy_configuration.rb +55 -3
  87. data/lib/contrast/config/application_configuration.rb +114 -15
  88. data/lib/contrast/config/assess_configuration.rb +106 -12
  89. data/lib/contrast/config/assess_rules_configuration.rb +44 -3
  90. data/lib/contrast/config/base_configuration.rb +1 -0
  91. data/lib/contrast/config/certification_configuration.rb +74 -3
  92. data/lib/contrast/config/exception_configuration.rb +61 -3
  93. data/lib/contrast/config/heap_dump_configuration.rb +101 -17
  94. data/lib/contrast/config/inventory_configuration.rb +64 -3
  95. data/lib/contrast/config/logger_configuration.rb +46 -3
  96. data/lib/contrast/config/protect_rule_configuration.rb +36 -9
  97. data/lib/contrast/config/protect_rules_configuration.rb +120 -17
  98. data/lib/contrast/config/request_audit_configuration.rb +68 -3
  99. data/lib/contrast/config/ruby_configuration.rb +96 -22
  100. data/lib/contrast/config/sampling_configuration.rb +76 -10
  101. data/lib/contrast/config/server_configuration.rb +56 -11
  102. data/lib/contrast/configuration.rb +6 -3
  103. data/lib/contrast/logger/cef_log.rb +151 -0
  104. data/lib/contrast/utils/hash_digest.rb +14 -6
  105. data/lib/contrast/utils/log_utils.rb +114 -0
  106. data/lib/contrast/utils/middleware_utils.rb +6 -7
  107. data/lib/contrast/utils/net_http_base.rb +12 -9
  108. data/lib/contrast/utils/patching/policy/patch_utils.rb +0 -4
  109. data/lib/contrast.rb +4 -3
  110. data/ruby-agent.gemspec +1 -1
  111. data/service_executables/VERSION +1 -1
  112. data/service_executables/linux/contrast-service +0 -0
  113. data/service_executables/mac/contrast-service +0 -0
  114. metadata +41 -21
  115. data/lib/contrast/agent/assess/rule/response/cachecontrol_rule.rb +0 -184
  116. data/lib/contrast/agent/assess/rule/response/clickjacking_rule.rb +0 -66
  117. data/lib/contrast/agent/assess/rule/response/x_content_type_rule.rb +0 -52
  118. data/lib/contrast/agent/assess/rule/response/x_xss_protection_rule.rb +0 -53
  119. data/lib/contrast/extension/kernel.rb +0 -54
@@ -2,7 +2,7 @@
2
2
  # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
3
3
  # frozen_string_literal: true
4
4
 
5
- require 'contrast/agent/assess/rule/response/base_rule'
5
+ require 'contrast/agent/assess/rule/response/header_rule'
6
6
  require 'contrast/utils/string_utils'
7
7
 
8
8
  module Contrast
@@ -11,27 +11,22 @@ module Contrast
11
11
  module Rule
12
12
  module Response
13
13
  # These rules check that the HTTP Headers include CSP header types
14
- class CspHeaderInsecure < BaseRule
14
+ class CspHeaderInsecure < HeaderRule
15
15
  def rule_id
16
16
  'csp-header-insecure'
17
17
  end
18
18
 
19
19
  protected
20
20
 
21
- CSP_HEADERS = %w[CONTENT_SECURITY_POLICY X_CONTENT_SECURITY_POLICY X_WEBKIT_CSP].cs__freeze
21
+ HEADER_KEYS = %w[Content-Security-Policy X-Content-Security-Policy X-Webkit-CSP].cs__freeze
22
+ DEFAULT_SAFE = false
22
23
  SETTINGS = %w[
23
24
  base-uri child-src default-src connect-src frame-src media-src object-src script-src
24
25
  style-src form-action frame-ancestors plugin-types reflected-xss referer
25
26
  ].cs__freeze
26
27
  UNSAFE_VALUE_REGEXP = /^unsafe-(?:inline|eval)$/.cs__freeze
27
28
  ASTERISK_REGEXP = /[*]/.cs__freeze
28
-
29
- # Rules discern which responses they can/should analyze.
30
- #
31
- # @param response [Contrast::Agent::Response] the response of the application
32
- def analyze_response? response
33
- super && headers?(response)
34
- end
29
+ SAFE_REFLECTED_XSS = /1/.cs__freeze
35
30
 
36
31
  # Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
37
32
  #
@@ -39,16 +34,19 @@ module Contrast
39
34
  # @return [Contrast::Utils::ObjectShare::EMPTY_STRING, nil] if CSP Header is not found
40
35
  def violated? response
41
36
  settings = {}
42
- csp_hash = get_csp_header_values(response.headers)
37
+ csp_hash = get_header_value(response)
43
38
  return if csp_hash.nil?
44
39
 
45
40
  SETTINGS.each do |setting_attr|
41
+ # default src has to be checked all other keys may be missing
42
+ next unless csp_hash.key?(setting_attr) || setting_attr == 'default-src'
43
+
46
44
  value = csp_hash[setting_attr]
47
45
  key = convert_key(setting_attr)
48
46
  settings["#{ key }Secure"] = !value.nil? && value_secure?(value) && value_safe?(value)
49
47
  settings["#{ key }Value"] = value.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : value
50
48
  end
51
- { DATA => settings }
49
+ evidence(settings) if settings.value?(false)
52
50
  end
53
51
 
54
52
  # Get the CSP values from and transforms them to key value hash
@@ -58,9 +56,10 @@ module Contrast
58
56
  #
59
57
  # @param headers [Hash] the response of the application
60
58
  # @return [Array, nil] array of CSP header values
61
- def get_csp_header_values headers
59
+ def get_header_value response
62
60
  csp_hash = {}
63
- CSP_HEADERS.each do |header_key|
61
+ headers = response.headers
62
+ HEADER_KEYS.each do |header_key|
64
63
  next unless headers[header_key]&.length&.positive?
65
64
 
66
65
  values = headers[header_key].split(Contrast::Utils::ObjectShare::SEMICOLON)
@@ -78,7 +77,7 @@ module Contrast
78
77
  end
79
78
 
80
79
  def value_safe? value
81
- UNSAFE_VALUE_REGEXP.match(value).nil?
80
+ UNSAFE_VALUE_REGEXP.match(value).nil? || !SAFE_REFLECTED_XSS.match(value).nil?
82
81
  end
83
82
 
84
83
  # Converts the CSP key to camelcase to be used as key for evidence object
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2022 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/agent/assess/rule/response/base_rule'
4
+ require 'contrast/agent/assess/rule/response/header_rule'
5
5
  require 'contrast/utils/string_utils'
6
6
 
7
7
  module Contrast
@@ -10,34 +10,14 @@ module Contrast
10
10
  module Rule
11
11
  module Response
12
12
  # These rules check that the HTTP Headers include CSP header types
13
- class CspHeaderMissing < BaseRule
13
+ class CspHeaderMissing < HeaderRule
14
14
  def rule_id
15
15
  'csp-header-missing'
16
16
  end
17
17
 
18
- protected
19
-
20
- CSP_HEADERS = %w[CONTENT_SECURITY_POLICY X_CONTENT_SECURITY_POLICY X_WEBKIT_CSP].cs__freeze
21
-
22
- DATA = 'data'.cs__freeze
23
-
24
- # Rules discern which responses they can/should analyze.
25
- #
26
- # @param response [Contrast::Agent::Response] the response of the application
27
- def analyze_response? response
28
- super && headers?(response)
29
- end
30
-
31
- # Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
32
- #
33
- # @param response [Contrast::Agent::Response] the response of the application
34
- # @return [Contrast::Utils::ObjectShare::EMPTY_STRING, nil] if CSP Header is not found
35
- def violated? response
36
- response_headers = response.headers
37
- return if CSP_HEADERS.any? { |header_key| response_headers[header_key]&.length&.positive? }
38
-
39
- { DATA => Contrast::Utils::ObjectShare::EMPTY_STRING }
40
- end
18
+ HEADER_KEYS = %w[Content-Security-Policy X-Content-Security-Policy X-Webkit-CSP].cs__freeze
19
+ ACCEPTED_VALUES = [/(.)/].cs__freeze
20
+ DEFAULT_SAFE = false
41
21
  end
42
22
  end
43
23
  end
@@ -0,0 +1,29 @@
1
+ # Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'rails'
5
+
6
+ module Contrast
7
+ module Agent
8
+ module Assess
9
+ module Rule
10
+ module Response
11
+ module Framework
12
+ # Rails 7 supports managing potential unsafe Headers
13
+ # this module contains methods for checking if Rails 7 supercedes our rules
14
+ module RailsSupport
15
+ RAILS_VERSION = Gem::Version.new('7.0.0')
16
+
17
+ def framework_supported?
18
+ rails_version = ::Rails.version
19
+ return false unless !!rails_version
20
+
21
+ Gem::Version.new(rails_version) >= RAILS_VERSION
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,70 @@
1
+ # Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'rack'
5
+ require 'contrast/agent/reporting/reporting_utilities/dtm_message'
6
+ require 'contrast/utils/hash_digest'
7
+ require 'contrast/utils/preflight_util'
8
+ require 'contrast/utils/string_utils'
9
+ require 'contrast/agent/assess/rule/response/base_rule'
10
+
11
+ module Contrast
12
+ module Agent
13
+ module Assess
14
+ module Rule
15
+ module Response
16
+ # These rules check the content of the HTTP Response to determine if something was set incorrectly or
17
+ # insecurely in it.
18
+ class HeaderRule < BaseRule
19
+ HEADER_TYPE = 'header'
20
+
21
+ # Rules discern which responses they can/should analyze.
22
+ #
23
+ # @param response [Contrast::Agent::Response] the response of the application
24
+ def analyze_response? response
25
+ super && headers?(response)
26
+ end
27
+
28
+ # Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
29
+ #
30
+ # @param response [Contrast::Agent::Response] the response of the application
31
+ # @return [Hash, nil] the evidence required to prove the violation of the rule
32
+ def violated? response
33
+ header_value = get_header_value(response)
34
+ if header_value
35
+ return evidence(header_value) unless valid_header?(header_value)
36
+ else
37
+ return evidence(header_value) unless cs__class::DEFAULT_SAFE
38
+ end
39
+ nil
40
+ end
41
+
42
+ # Determine if a response has headers.
43
+ #
44
+ # @param response [Contrast::Agent::Response] the response of the application
45
+ # @return [Boolean]
46
+ def headers? response
47
+ response.headers&.any?
48
+ end
49
+
50
+ protected
51
+
52
+ def get_header_value response
53
+ response_headers = response.headers
54
+ values = response_headers.values_at(*cs__class::HEADER_KEYS, *cs__class::HEADER_KEYS.map(&:to_sym))
55
+ values.compact.first
56
+ end
57
+
58
+ # Determine if the value of the Response Header has a valid value
59
+ #
60
+ # @param header [Contrast::Agent::Response] a response header
61
+ # @return [Boolean] whether the header value is valid
62
+ def valid_header? header
63
+ cs__class::ACCEPTED_VALUES.any? { |val| val.match(header) }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,6 +1,9 @@
1
1
  # Copyright (c) 2022 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/agent/assess/rule/response/header_rule'
5
+ require 'contrast/utils/string_utils'
6
+
4
7
  module Contrast
5
8
  module Agent
6
9
  module Assess
@@ -8,49 +11,22 @@ module Contrast
8
11
  module Response
9
12
  # This rule checks if the HTTP Headers include HSTS header and ensures that the max-age value
10
13
  # is set to a value greater than 0.
11
- class HSTSHeader < BaseRule
14
+ class HSTSHeader < HeaderRule
12
15
  def rule_id
13
16
  'hsts-header-missing'
14
17
  end
15
18
 
16
19
  protected
17
20
 
18
- HEADER_KEY = 'Strict-Transport-Security'
19
- HEADER_KEY_SYM = HEADER_KEY.to_sym
20
- MAX_AGE = 'max-age'
21
- MAX_AGE_SYM = MAX_AGE.to_sym
22
- # Rules discern which responses they can/should analyze.
23
- #
24
- # @param response [Contrast::Agent::Response] the response of the application
25
- def analyze_response? response
26
- super && response.headers.cs__is_a?(Hash)
27
- end
28
-
29
- # Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
30
- #
31
- # @param response [Contrast::Agent::Response] the response of the application
32
- # @return [Hash<data: Contrast::Utils::ObjectShare::EMPTY_STRING, String>, nil] return string
33
- # representation of the max_age
34
- def violated? response
35
- headers = response.headers
36
- target = headers[HEADER_KEY] || headers[HEADER_KEY_SYM]
37
- # this rule is safe by default if no target => no evidence
38
- # if the property max_age is not positive or absent then the rule is violated
39
- return unless target
40
-
41
- max_age = target[MAX_AGE] || target[MAX_AGE_SYM]
42
- return if max_age.to_i.positive?
43
-
44
- evidence max_age
45
- end
21
+ HEADER_KEYS = %w[Strict-Transport-Security].cs__freeze
22
+ ACCEPTED_VALUES = [/max-age=(\.)?\d+(\.\d*)?/].cs__freeze
23
+ DEFAULT_SAFE = true
46
24
 
47
- # returns evidence that the max_age is negative or absent
48
- #
49
- # @param max_age [String] String representation of the max-age value to which the header is set
50
- # @return [Hash<data: Contrast::Utils::ObjectShare::EMPTY_STRING, String>] return string representation of
51
- # the max_age
52
- def evidence max_age
53
- { data: max_age.to_s }
25
+ def evidence data
26
+ # get only the value of the max-age property
27
+ val = data&.split('=')&.last
28
+ val = Contrast::Utils::ObjectShare::EMPTY_STRING if val.nil? || val == 'max-age'
29
+ { DATA => val }
54
30
  end
55
31
  end
56
32
  end
@@ -12,6 +12,7 @@ module Contrast
12
12
  # These rules check the content of the HTTP Response to determine if the body contains a form which
13
13
  # incorrectly sets the action attribute.
14
14
  class ParametersPollution < BaseRule
15
+ include BodyRule
15
16
  def rule_id
16
17
  'parameter-pollution'
17
18
  end
@@ -31,7 +32,7 @@ module Contrast
31
32
  # @return [Hash, nil] the evidence required to prove the violation of the rule
32
33
  def violated? response
33
34
  body = response.body
34
- forms = forms(body)
35
+ forms = html_elements(body, FORM_START_REGEXP, true)
35
36
  forms.each do |form|
36
37
  # Because TeamServer will reject any subsequent form on the same page due to deduplication, we can
37
38
  # skip out on the first violation.
@@ -0,0 +1,26 @@
1
+ # Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/assess/rule/response/header_rule'
5
+ require 'contrast/utils/string_utils'
6
+
7
+ module Contrast
8
+ module Agent
9
+ module Assess
10
+ module Rule
11
+ module Response
12
+ # These rules check the content of the HTTP Response to determine if the response contains the needed header
13
+ class XContentType < HeaderRule
14
+ def rule_id
15
+ 'xcontenttype-header-missing'
16
+ end
17
+
18
+ HEADER_KEYS = %w[X-Content-Type-Options].cs__freeze
19
+ ACCEPTED_VALUES = [/^nosniff/i].cs__freeze
20
+ DEFAULT_SAFE = false
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/assess/rule/response/header_rule'
5
+ require 'contrast/utils/string_utils'
6
+ require 'contrast/agent/assess/rule/response/framework/rails_support'
7
+ require 'rails'
8
+
9
+ module Contrast
10
+ module Agent
11
+ module Assess
12
+ module Rule
13
+ module Response
14
+ # These rules check the content of the HTTP Response to determine if the response contains the needed header
15
+ class XXssProtection < HeaderRule
16
+ include Framework::RailsSupport
17
+
18
+ def rule_id
19
+ 'xxssprotection-header-disabled'
20
+ end
21
+
22
+ HEADER_KEYS = %w[X-XSS-Protection].cs__freeze
23
+ ACCEPTED_VALUES = [/^1/].cs__freeze
24
+ DEFAULT_SAFE = true
25
+
26
+ protected
27
+
28
+ def analyze_response? response
29
+ !framework_supported? && super
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -173,6 +173,7 @@ module Contrast
173
173
  request_handler.send_activity_messages # TODO: RUBY-1438 -- remove
174
174
  end
175
175
  end
176
+ # unsuccessful attack
176
177
  rescue StandardError => e
177
178
  raise e if security_exception?(e)
178
179
 
@@ -29,14 +29,13 @@ module Contrast
29
29
  # there are no require time side effects of loading our core
30
30
  # extensions.
31
31
  def apply_direct_patches!
32
- @_apply_direct_patches ||= begin
32
+ @_apply_direct_patches ||= begin # TODO: RUBY-1541 - put 'kernel' back
33
33
  paths = %w[
34
34
  array
35
35
  basic_object
36
36
  module
37
37
  fiber_track
38
38
  hash
39
- kernel
40
39
  marshal_module
41
40
  regexp
42
41
  string
@@ -75,7 +74,6 @@ module Contrast
75
74
  def apply_require_patches!
76
75
  @_apply_require_patches ||= begin
77
76
  require 'contrast/extension/thread'
78
- require 'contrast/extension/kernel'
79
77
  true
80
78
  rescue LoadError, StandardError => e
81
79
  logger.error('failed instrumenting apply_require_patches!', e)
@@ -49,15 +49,11 @@ module Contrast
49
49
  ].cs__freeze
50
50
 
51
51
  def enter_method_scope! method_policy
52
- method_policy.scopes_to_enter.each do |scope|
53
- enter_scope!(scope)
54
- end
52
+ contrast_enter_method_scopes! method_policy.scopes_to_enter
55
53
  end
56
54
 
57
55
  def exit_method_scope! method_policy
58
- method_policy.scopes_to_exit.each do |scope|
59
- exit_scope!(scope)
60
- end
56
+ contrast_exit_method_scopes! method_policy.scopes_to_exit
61
57
  end
62
58
 
63
59
  # @param mod [Module] the module in which the patch should be
@@ -147,7 +147,7 @@ module Contrast
147
147
  return
148
148
  end
149
149
 
150
- patch_methods status, module_data, module_policy
150
+ patch_methods(status, module_data, module_policy)
151
151
  rescue StandardError => e
152
152
  status&.failed_patch!
153
153
  logger.warn('Patching failed', e, module: module_data.mod_name)
@@ -0,0 +1,94 @@
1
+ # Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/reporting/input_analysis/input_type'
5
+ require 'contrast/agent/reporting/input_analysis/score_level'
6
+ require 'contrast/agent/reporting/input_analysis/input_analysis'
7
+ require 'contrast/agent/protect/rule/sqli/sqli_input_classification'
8
+ require 'json'
9
+
10
+ module Contrast
11
+ module Agent
12
+ module Protect
13
+ # InputAnalyzer will extract input form current request context and will analyze it.
14
+ # This will be used in for the SQLI and CMDI worth_watching_v2 implementations.
15
+ module InputAnalyzer
16
+ class << self
17
+ include Contrast::Agent::Reporting::InputType
18
+ include Contrast::Agent::Reporting::ScoreLevel
19
+ include Contrast::Agent::Protect::Rule::SqliWorthWatching
20
+
21
+ PROTECT_RULES = { sqli: 'sql-injection' }.cs__freeze
22
+
23
+ # This method with analyze the user input from the context of the
24
+ # current request and run each of the protect rules against all
25
+ # found input types
26
+ #
27
+ # @param request [Contrast::Agent::Request] current request context.
28
+ # @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
29
+ def analyse request
30
+ return unless Contrast::SETTINGS.protect_state.enabled
31
+ return if request.nil?
32
+
33
+ inputs = extract_input request
34
+ return unless inputs
35
+
36
+ input_analysis = Contrast::Agent::Reporting::InputAnalysis.new
37
+ input_analysis.request = request
38
+ # each rule against each input
39
+ input_classification inputs, input_analysis
40
+ input_analysis
41
+ end
42
+
43
+ private
44
+
45
+ # classify input by rule implementation of worth_watching_v2 for the rules supporting it.
46
+ #
47
+ # @param inputs [String, Array<String>] user input to be analysed.
48
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Here we will keep all the results
49
+ # for each protect rule.
50
+ # @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
51
+ def input_classification inputs, input_analysis
52
+ # key = input type, value = user_input
53
+ inputs.each do |input_type, value|
54
+ PROTECT_RULES.each do |_key, rule_id|
55
+ # check if rule is enabled
56
+ next unless Contrast::PROTECT.rule(rule_id).enabled?
57
+
58
+ # start with sqli rule
59
+ case rule_id
60
+ when PROTECT_RULES[:sqli]
61
+ Contrast::Agent::Protect::Rule::SqliInputClassification.classify input_type, value, input_analysis
62
+ else
63
+ return nil
64
+ end
65
+ end
66
+ end
67
+ input_analysis
68
+ end
69
+
70
+ # Extract the inputs from the request context and label them with Protect
71
+ # input type tags. Each tag will contain one or more user inputs.
72
+ #
73
+ # This methods is to be expanded and modified as needed by other Protect rules
74
+ # and sub-rules for their requirements.
75
+ #
76
+ # @param request [Contrast::Agent::Request] current request context.
77
+ # @return inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
78
+ def extract_input request
79
+ inputs = {}
80
+ inputs[BODY] = request.body
81
+ inputs[COOKIE_NAME] = request.cookies.keys
82
+ inputs[COOKIE_VALUE] = request.cookies.values
83
+ inputs[HEADER] = request.headers
84
+ inputs[PARAMETER_NAME] = request.parameters.keys
85
+ inputs[PARAMETER_VALUE] = request.parameters.values
86
+ inputs[QUERYSTRING] = request.query_string
87
+ inputs.compact!
88
+ inputs
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -173,6 +173,19 @@ module Contrast
173
173
  context.activity.results << result if result
174
174
  end
175
175
 
176
+ # With this we log to CEF
177
+ #
178
+ # @param result [Contrast::Api::Dtm::AttackResult]
179
+ # @param attack [Symbol] the type of message we want to send
180
+ # @param value [String] the input value we want to log
181
+ def cef_logging result, attack = :ineffective_attack, value = nil
182
+ sample_to_json = Contrast::Api::Dtm::RaspRuleSample.to_controlled_hash result.samples[0]
183
+ outcome = Contrast::Api::Dtm::AttackResult::ResponseType.get_name_by_tag(result.response)
184
+ input_type = extract_input_type sample_to_json[:user_input].input_type
185
+ input_value = value || sample_to_json[:user_input].value
186
+ cef_logger.send(attack, result.rule_id, outcome, input_type, input_value)
187
+ end
188
+
176
189
  protected
177
190
 
178
191
  def mode_from_settings
@@ -232,7 +245,13 @@ module Contrast
232
245
 
233
246
  def update_perimeter_attack_response context, ia_result, result
234
247
  if mode == Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER
235
- result.response = Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED_AT_PERIMETER
248
+ result.response = if ia_result&.rule_id == Contrast::Agent::Protect::Rule::Sqli::NAME
249
+ # Block At Perimeter mode has been deprecated in sqli_worth_watching_v2
250
+ # and should be treated equivalent to Blocked mode if set
251
+ Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED
252
+ else
253
+ Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED_AT_PERIMETER
254
+ end
236
255
  log_rule_matched(context, ia_result, result.response)
237
256
  elsif ia_result.nil? || ia_result.attack_count.zero?
238
257
  result.response = Contrast::Api::Dtm::AttackResult::ResponseType::PROBED
@@ -293,6 +312,14 @@ module Contrast
293
312
  result: response)
294
313
  end
295
314
 
315
+ # This method returns the symbol for the enum
316
+ #
317
+ # @param enum [Enumerable]
318
+ # @return [Symbol]
319
+ def extract_input_type enum
320
+ Contrast::Api::Dtm::UserInput::InputType.get_name_by_tag enum
321
+ end
322
+
296
323
  private
297
324
 
298
325
  def log_rule_probed _context, ia_result
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/agent/protect/rule/base'
5
+ require 'contrast/components/logger'
5
6
 
6
7
  module Contrast
7
8
  module Agent
@@ -10,6 +11,8 @@ module Contrast
10
11
  # Encapsulate common code for protect rules that do their
11
12
  # input analysis on Speedracer rather in ruby code
12
13
  class BaseService < Contrast::Agent::Protect::Rule::Base
14
+ include Contrast::Components::Logger::InstanceMethods
15
+
13
16
  def rule_name
14
17
  'base-service'
15
18
  end
@@ -41,6 +44,7 @@ module Contrast
41
44
  result = find_postfilter_attacker(context, nil)
42
45
  return unless result&.samples&.any?
43
46
 
47
+ cef_logging result
44
48
  append_to_activity(context, result)
45
49
  return unless result.response == :BLOCKED
46
50
 
@@ -83,7 +87,12 @@ module Contrast
83
87
  def find_postfilter_attacker context, potential_attack_string, **kwargs
84
88
  ia_results = gather_ia_results(context)
85
89
  ia_results.select! do |ia_result|
86
- ia_result.score_level == Contrast::Api::Settings::InputAnalysisResult::ScoreLevel::DEFINITEATTACK
90
+ ia_result.score_level == if ia_result.rule_id == Contrast::Agent::Protect::Rule::Sqli::NAME
91
+ Contrast::Agent::Reporting::ScoreLevel::WORTHWATCHING
92
+ else
93
+ # legacy implementation for DEFINITEATATACK
94
+ Contrast::Api::Settings::InputAnalysisResult::ScoreLevel::DEFINITEATTACK
95
+ end
87
96
  end
88
97
  find_attacker_with_results(context, potential_attack_string, ia_results, **kwargs)
89
98
  end
@@ -39,6 +39,8 @@ module Contrast
39
39
  return unless result
40
40
 
41
41
  append_to_activity(context, result)
42
+ cef_logging result, :successful_attack
43
+
42
44
  return unless blocked?
43
45
 
44
46
  raise Contrast::SecurityException.new(self,