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
@@ -0,0 +1,88 @@
1
+ #include <ruby.h>
2
+
3
+ /* Calls to Contrast modules and classes */
4
+ VALUE scope_interface;
5
+ VALUE scope_inst_methods, scope_mod, scope_klass;
6
+
7
+ /* IVs */
8
+ static VALUE rb_iv_cntr_scope;
9
+ static VALUE rb_iv_dslr_scope;
10
+ static VALUE rb_iv_split_scope;
11
+
12
+ /* Constants */
13
+ static VALUE rb_const_ec;
14
+ static VALUE rb_const_mon;
15
+ static VALUE rb_const_ec_keys;
16
+
17
+ /* Symbols */
18
+ static VALUE rb_sym_scope_mod;
19
+ static VALUE rb_sym_contrast;
20
+ static VALUE rb_sym_deserialization;
21
+ static VALUE rb_sym_split;
22
+
23
+ /* methods names - only reusable ones */
24
+ VALUE rb_method_name_init;
25
+ VALUE rb_method_name_in_scope;
26
+ VALUE rb_method_name_enter_scope;
27
+ VALUE rb_method_name_exit_scope;
28
+ VALUE rb_method_name_scope_for_current_ec;
29
+ VALUE rb_method_name_in_cntr_scope;
30
+ VALUE rb_method_name_enter_cntr_scope;
31
+ VALUE rb_method_name_exit_cntr_scope;
32
+ VALUE rb_method_name_in_dslr_scope;
33
+ VALUE rb_method_name_enter_dslr_scope;
34
+ VALUE rb_method_name_exit_dslr_scope;
35
+ VALUE rb_method_name_in_split_scope;
36
+ VALUE rb_method_name_enter_split_scope;
37
+ VALUE rb_method_name_exit_split_scope;
38
+ VALUE rb_method_name_split_scope_depth;
39
+
40
+ /* Scope class */
41
+ VALUE contrast_scope_klass_init(VALUE self, VALUE args);
42
+ VALUE in_cntr_scope(VALUE self, VALUE args);
43
+ VALUE enter_cntr_scope(VALUE self, VALUE args);
44
+ VALUE exit_cntr_scope(VALUE self, VALUE args);
45
+ VALUE in_split_scope(VALUE self, VALUE args);
46
+ VALUE enter_split_scope(VALUE self, VALUE args);
47
+ VALUE exit_split_scope(VALUE self, VALUE args);
48
+ VALUE split_scope_depth(VALUE self, VALUE args);
49
+ VALUE in_dsrl_scope_scope(VALUE self, VALUE args);
50
+ VALUE enter_dsrl_scope(VALUE self, VALUE args);
51
+ VALUE exit_dsrl_scope(VALUE self, VALUE args);
52
+ VALUE scope_klass_in_scope(VALUE self, VALUE method_scope_sym);
53
+ VALUE scope_klass_enter_scope(VALUE self, VALUE method_scope_sym);
54
+ VALUE scope_klass_exit_scope(VALUE self, VALUE method_scope_sym);
55
+
56
+ /* Scope interface */
57
+ VALUE contrast_scope_interface_init(VALUE self, VALUE args);
58
+ VALUE contrast_scope_for_current_ec(VALUE self, VALUE args);
59
+
60
+ /* Scope instance methods */
61
+ VALUE inst_methods_in_cntr_scope(VALUE self, VALUE args);
62
+ VALUE inst_methods_enter_cntr_scope(VALUE self, VALUE args);
63
+ VALUE inst_methods_exit_cntr_scope(VALUE self, VALUE args);
64
+ VALUE inst_methods_in_split_scope(VALUE self, VALUE args);
65
+ VALUE inst_methods_enter_split_scope(VALUE self, VALUE args);
66
+ VALUE inst_methods_exit_split_scope(VALUE self, VALUE args);
67
+ VALUE inst_methods_split_scope_depth(VALUE self, VALUE args);
68
+ VALUE inst_methods_in_dsrl_scope(VALUE self, VALUE args);
69
+ VALUE inst_methods_enter_dsrl_scope(VALUE self, VALUE args);
70
+ VALUE inst_methods_exit_dsrl_scope(VALUE self, VALUE args);
71
+ VALUE inst_methods_in_scope(VALUE self, VALUE method_scope_sym);
72
+ VALUE inst_methods_enter_scope(VALUE self, VALUE method_scope_sym);
73
+ VALUE inst_methods_exit_scope(VALUE self, VALUE method_scope_sym);
74
+
75
+ /* Scope components module */
76
+ VALUE scope_mod_sweep_dead_ecs(VALUE self, VALUE args);
77
+ VALUE inst_methods_enter_method_scope(VALUE self, VALUE scopes_to_enter);
78
+ VALUE inst_methods_exit_method_scope(VALUE self, VALUE scopes_to_exit);
79
+
80
+ /* Helpers */
81
+ VALUE is_in_scope(int scope);
82
+ VALUE get_ec();
83
+ VALUE rb_new_c_scope();
84
+ void rb_raise_scope_no_method_err(const VALUE method_scope_sym);
85
+ int scope_increase(int scope);
86
+ int scope_decrease(int scope);
87
+
88
+ void Init_cs__scope(void);
@@ -0,0 +1,5 @@
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
+ $TO_MAKE = File.basename(__dir__)
5
+ require_relative '../extconf_common'
@@ -15,20 +15,27 @@ module Contrast
15
15
  module Assess
16
16
  # This class holds the data about an event in the application We'll use it to build an event that TeamServer can
17
17
  # consume if the object to which this event belongs ends in a trigger.
18
- #
19
- # @attr_reader event_id [Integer] the atomic id of this event
20
- # @attr_reader policy_node [Contrast::Agent::Assess::Policy::PolicyNode] the node that governs this event.
21
- # @attr_reader stack_trace [Array<String>] the execution stack at the time the method for this event was invoked
22
- # @attr_reader time [Integer] the time, in epoch ms, when this event was created
23
- # @attr_reader thread [Integer] the object id of the thread on which this event was generated
24
- # @attr_reader object [Contrast::Agent::Assess::ContrastObject] the safe representation of the Object on which
25
- # the method was invoked
26
- # @attr_reader ret [Contrast::Agent::Assess::ContrastObject] the safe representation of the Return of the invoked
27
- # method
28
- # @attr_reader args [Array<Contrast::Agent::Assess::ContrastObject>] the safe representation of the Arguments
29
- # with which the method was invoked
30
18
  class ContrastEvent
31
- attr_reader :event_id, :policy_node, :stack_trace, :time, :thread, :object, :ret, :args, :tags
19
+ # @return [Integer] the atomic id of this event
20
+ attr_reader :event_id
21
+ # @return [Contrast::Agent::Assess::Policy::PolicyNode] the node that governs this event.
22
+ attr_reader :policy_node
23
+ # @return [Array<String>] the execution stack at the time the method for this event was invoked
24
+ attr_reader :stack_trace
25
+ # @return [Integer] the time, in epoch ms, when this event was created
26
+ attr_reader :time
27
+ # @return [Integer] the object id of the thread on which this event was generated
28
+ attr_reader :thread
29
+ # @return [Contrast::Agent::Assess::ContrastObject] the safe representation of the Object on which the method
30
+ # was invoked
31
+ attr_reader :object
32
+ # @return [Contrast::Agent::Assess::ContrastObject] the safe representation of the Return of the invoked method
33
+ attr_reader :ret
34
+ # @return [Array<Contrast::Agent::Assess::ContrastObject, nil>] the safe representation of the Arguments with
35
+ # which the method was invoked
36
+ attr_reader :args
37
+ # @return [Hash<Contrast::Agent::Assess::Tag>]
38
+ attr_reader :tags
32
39
 
33
40
  # We need this to track the parent id's of events to build up a flow chart of the finding
34
41
  @atomic_id = 0
@@ -42,8 +42,11 @@ module Contrast
42
42
  end
43
43
  end
44
44
 
45
+ # Is the object this represents tracked or not?
46
+ #
47
+ # @return [Boolean]
45
48
  def tracked?
46
- tags&.any?
49
+ !!tags&.any?
47
50
  end
48
51
  end
49
52
  end
@@ -118,11 +118,8 @@ module Contrast
118
118
  # it is an interesting security event that has a meaningful
119
119
  # change.
120
120
  def tagger?
121
- @_tagger ||= begin
122
- has_tags = tags&.any?
123
- has_untags = untags&.any?
124
- has_tags || has_untags
125
- end
121
+ @_tagger = tags&.any? || untags&.any? if @_tagger.nil?
122
+ @_tagger
126
123
  end
127
124
  end
128
125
  end
@@ -37,6 +37,8 @@ module Contrast
37
37
  end
38
38
 
39
39
  def captures_tagger propagation_node, preshift, ret, _block
40
+ return unless ret
41
+
40
42
  idx = 0
41
43
  while idx < ret.length
42
44
  return_value = ret[idx]
@@ -8,6 +8,9 @@ require 'contrast/utils/object_share'
8
8
  require 'contrast/utils/sha256_builder'
9
9
  require 'contrast/utils/assess/trigger_method_utils'
10
10
  require 'contrast/agent/assess/events/event_data'
11
+ require 'contrast/agent/reporting/reporting_events/preflight'
12
+ require 'contrast/agent/reporting/reporting_events/preflight_message'
13
+ require 'contrast/agent/reporting/reporting_events/route_discovery'
11
14
  require 'contrast/agent/reporting/reporting_utilities/reporting_storage'
12
15
 
13
16
  module Contrast
@@ -154,7 +157,7 @@ module Contrast
154
157
 
155
158
  new_preflight = Contrast::Agent::Reporting::Preflight.new
156
159
  new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new
157
- new_preflight_message.routes << request
160
+ new_preflight_message.routes << Contrast::Agent::Reporting::RouteDiscovery.convert(request.route)
158
161
  new_preflight_message.hash_code = hash_code
159
162
  new_preflight_message.data = "#{ trigger_node.rule_id },#{ hash_code }"
160
163
  new_preflight.messages << new_preflight_message
@@ -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/body_rule'
5
5
  require 'contrast/utils/string_utils'
6
6
 
7
7
  module Contrast
@@ -11,7 +11,8 @@ module Contrast
11
11
  module Response
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 autocomplete attribute.
14
- class Autocomplete < BaseRule
14
+ class AutoComplete < BaseRule
15
+ include BodyRule
15
16
  def rule_id
16
17
  'autocomplete-missing'
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.
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'rack'
5
+ require 'json'
5
6
  require 'contrast/agent/reporting/reporting_utilities/dtm_message'
6
7
  require 'contrast/utils/hash_digest'
7
8
  require 'contrast/utils/preflight_util'
@@ -41,9 +42,6 @@ module Contrast
41
42
  protected
42
43
 
43
44
  DATA = 'data'.cs__freeze
44
- HTML_PROP = 'html'.cs__freeze
45
- START_PROP = 'start'.cs__freeze
46
- END_PROP = 'end'.cs__freeze
47
45
 
48
46
  # Rules discern which responses they can/should analyze.
49
47
  #
@@ -60,10 +58,15 @@ module Contrast
60
58
 
61
59
  # Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
62
60
  #
63
- # @param _response [Contrast::Agent::Response] the response of the application
61
+ # @param response [Contrast::Agent::Response] the response of the application
64
62
  # @return [Hash, nil] the evidence required to prove the violation of the rule
65
63
  def violated? _response; end
66
64
 
65
+ def evidence data = Contrast::Utils::ObjectShare::EMPTY_STRING
66
+ data = Contrast::Utils::ObjectShare::EMPTY_STRING if data.nil?
67
+ { DATA => data }
68
+ end
69
+
67
70
  # Convert the given evidence into a finding. The rule will populate this evidence with each of the
68
71
  #
69
72
  # @param evidence [Hash] the properties required to build this finding.
@@ -87,7 +90,11 @@ module Contrast
87
90
  # @param finding [Contrast::Api::Dtm::Finding] finding to attach the evidence to
88
91
  def build_evidence evidence, finding
89
92
  evidence.each_pair do |key, value|
90
- finding.properties[key] = Contrast::Utils::StringUtils.force_utf8(value)
93
+ finding.properties[key] = if value.cs__is_a?(Hash)
94
+ Contrast::Utils::StringUtils.protobuf_format(value.to_json)
95
+ else
96
+ Contrast::Utils::StringUtils.protobuf_format(value)
97
+ end
91
98
  end
92
99
  end
93
100
 
@@ -115,80 +122,6 @@ module Contrast
115
122
  def valid_content_type? type
116
123
  !type || [/json/i, /xml/i].none? { |invalid_content| type =~ invalid_content }
117
124
  end
118
-
119
- # Determine if a response has a body or not.
120
- #
121
- # @param response [Contrast::Agent::Response] the response of the application
122
- # @return [Boolean]
123
- def body? response
124
- Contrast::Utils::StringUtils.present?(response.body)
125
- end
126
-
127
- # Capture the information needed to build the properties of this finding by parsing out from the body
128
- #
129
- # @param body [String] the entire HTTP Response body
130
- # @param body_start [Integer] the start of the range to take from the body
131
- # @param body_close [Integer] the end of the range to take from the body
132
- # @param tag_stop [Integer] the index of the end of the html tag from its start
133
- # @return [Hash]
134
- def capture body, body_start, body_close, tag_stop
135
- tag = {}
136
- # Capture the 50 characters in front of the form, or up to the start if the form starts before 50.
137
- capture_start = body_start < 50 ? 0 : body_start - 50
138
- # Start is where the '<form' is in the body
139
- # 6 accounts for the characters in the form and the opening char
140
- # potential_form.index('>') accounts for finding the rest of the form
141
- # 50 accounts for the context to capture beyond
142
- capture_close = body_close + 50
143
- tag[HTML_PROP] = body[capture_start...capture_close]
144
- tag[START_PROP] = body_start < 50 ? body_start : 50
145
- tag[END_PROP] = tag[START_PROP] + 6 + tag_stop
146
- tag
147
- end
148
-
149
- # Find the forms in this body, if any, so as to determine if they violate this rule.
150
- #
151
- # @param body [String]
152
- # @return [Array<Hash>] the forms of this body, as well as their start and end indexes.
153
- def forms body
154
- forms = []
155
- body_start = 0
156
- # The instance of "<form" in the body may be a form. Turn them into chunks to check.
157
- potential_forms = body.split(form_start)
158
- potential_forms.each do |potential_form|
159
- # We can consider this a form if the next character is one of whitespace of form tag closing
160
- # characters.
161
- next unless potential_form
162
- next unless form_openings.any? { |opening| potential_form.start_with?(opening) }
163
-
164
- body_start = body.index(form_start, body_start)
165
- next unless body_start
166
-
167
- form_stop = potential_form.index('>').to_i
168
- next unless form_stop
169
-
170
- body_close = body_start + 6 + form_stop
171
- forms << capture(body, body_start, body_close, form_stop)
172
- body_start = body_close
173
- end
174
- forms
175
- end
176
-
177
- def form_start
178
- /<form/i
179
- end
180
-
181
- def form_openings
182
- [' ', "\n", "\r", "\t", '>']
183
- end
184
-
185
- # Determine if a response has headers.
186
- #
187
- # @param response [Contrast::Agent::Response] the response of the application
188
- # @return [Boolean]
189
- def headers? response
190
- response.headers.cs__is_a?(Hash)
191
- end
192
125
  end
193
126
  end
194
127
  end
@@ -0,0 +1,109 @@
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
+ module BodyRule
19
+ protected
20
+
21
+ HTML_PROP = 'html'.cs__freeze
22
+ START_PROP = 'start'.cs__freeze
23
+ END_PROP = 'end'.cs__freeze
24
+ FORM_START_REGEXP = /<form/i.cs__freeze
25
+ META_TYPE = 'meta'
26
+ PRAGMA = 'pragma'
27
+
28
+ # Rules discern which responses they can/should analyze.
29
+ #
30
+ # @param response [Contrast::Agent::Response] the response of the application
31
+ def analyze_response? response
32
+ super && body?(response)
33
+ end
34
+
35
+ # Determine if a response has a body or not.
36
+ #
37
+ # @param response [Contrast::Agent::Response] the response of the application
38
+ # @return [Boolean]
39
+ def body? response
40
+ Contrast::Utils::StringUtils.present?(response.body)
41
+ end
42
+
43
+ # Find the elements in this section, if any, so as to determine if they violate this rule.
44
+ #
45
+ # @param section [String,nil] html section to find element
46
+ # @param element_start_str [String] element to find in html section
47
+ # @return [Array<Hash>] the found elements of this section, as well as their start and end indexes.
48
+ def html_elements section, element_start_str = '', capture_overflow = false
49
+ elements = []
50
+ section_start = 0
51
+ return [] unless section
52
+
53
+ potential_elements(section, element_start_str).flatten.each do |potential_element|
54
+ next unless potential_element
55
+ next unless element_openings.any? { |opening| potential_element.starts_with?(opening) }
56
+
57
+ section_start = section.index(element_start_str, section_start)
58
+ next unless section_start
59
+
60
+ element_stop = potential_element.index('>').to_i
61
+ next unless element_stop
62
+
63
+ section_close = section_start + 6 + element_stop
64
+ elements << capture(section, section_start, section_close, element_stop, capture_overflow)
65
+ section_start = section_close
66
+ end
67
+ elements
68
+ end
69
+
70
+ def potential_elements section, element_start
71
+ section.split(element_start)
72
+ end
73
+
74
+ def element_openings
75
+ [' ', "\n", "\r", "\t", '>']
76
+ end
77
+
78
+ # Capture the information needed to build the properties of this finding by parsing out from the body
79
+ #
80
+ # @param body [String] the entire HTTP Response body
81
+ # @param body_start [Integer] the start of the range to take from the body
82
+ # @param body_close [Integer] the end of the range to take from the body
83
+ # @param tag_stop [Integer] the index of the end of the html tag from its start
84
+ # @return [Hash]
85
+ def capture body, body_start, body_close, tag_stop, overflow = false
86
+ tag = {}
87
+ # Capture the 50 characters in front of the form, or up to the start if the form starts before 50.
88
+ if overflow
89
+ capture_start = body_start < 50 ? 0 : body_start - 50
90
+ # Start is where the '<form' is in the body
91
+ # 6 accounts for the characters in the form and the opening char
92
+ # potential_form.index('>') accounts for finding the rest of the form
93
+ # 50 accounts for the context to capture beyond
94
+ capture_close = body_close + 50
95
+ tag[HTML_PROP] = body[capture_start...capture_close]
96
+ tag[START_PROP] = body_start < 50 ? body_start : 50
97
+ else
98
+ tag[HTML_PROP] = body[body_start...body_close]
99
+ tag[START_PROP] = body_start
100
+ end
101
+ tag[END_PROP] = tag[START_PROP] + 6 + tag_stop
102
+ tag
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,157 @@
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/agent/assess/rule/response/body_rule'
6
+ require 'contrast/agent/assess/rule/response/framework/rails_support'
7
+ require 'contrast/utils/string_utils'
8
+ require 'json'
9
+
10
+ module Contrast
11
+ module Agent
12
+ module Assess
13
+ module Rule
14
+ module Response
15
+ # These rules check the content of the HTTP Response to determine if the body or the headers include and/or
16
+ # set incorrectly the cache-control header
17
+ class CacheControl < HeaderRule
18
+ include BodyRule
19
+ include Framework::RailsSupport
20
+
21
+ def rule_id
22
+ 'cache-controls-missing'
23
+ end
24
+
25
+ protected
26
+
27
+ HEADER_KEYS = %w[Cache-Control].cs__freeze
28
+ ACCEPTED_VALUES = [/no-store/, /no-cache/].cs__freeze
29
+ DEFAULT_SAFE = false
30
+ META_START_STR = /<meta/i.cs__freeze
31
+ HEAD_TAG = /<head>/i.cs__freeze
32
+ NAME = 'cache-control'
33
+
34
+ # Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
35
+ #
36
+ # @param response [Contrast::Agent::Response] the response of the application
37
+ # @return [Hash, nil] the evidence required to prove the violation of the rule
38
+ def violated? response
39
+ has_header, evidence = process_header(response)
40
+ return evidence unless evidence.nil?
41
+
42
+ has_tag, evidence = process_body(response)
43
+ return evidence unless evidence.nil?
44
+ return {} if !has_header && !has_tag
45
+
46
+ nil
47
+ end
48
+
49
+ # Process Header value to determine if it violates rule
50
+ # @param response [Contrast::Agent::Response] the response of the application
51
+ # @return [Array[Boolean, Hash]] whether the header exists and the evidence hash or nil
52
+ def process_header response
53
+ # Rails 7 adds support for the cache_control header directly in the
54
+ # rack response, we should use that value
55
+ if framework_supported?
56
+ cache_control = response.rack_response.cache_control
57
+ has_header = !cache_control.empty?
58
+ not_valid = has_header && !((cache_control[:no_cache]) || (cache_control[:no_store]))
59
+ # evidence requires header value string, pull directly instead of rebuilding from hash
60
+ return has_header, evidence(HEADER_TYPE, NAME, cache_control_to_s(cache_control)) if not_valid
61
+ else
62
+ # This rule is violated if the header is not there is there,
63
+ # but the value is not 'no-store' or 'no-cache'
64
+ cache_control = get_header_value(response)
65
+ has_header = !!cache_control
66
+ not_valid = has_header && !valid_header?(cache_control)
67
+ return has_header, evidence(HEADER_TYPE, NAME, cache_control) if not_valid
68
+ end
69
+ [has_header, nil]
70
+ end
71
+
72
+ # Process Body to determine cache control exists as meta tag and if it violates rule
73
+ # @param response [Contrast::Agent::Response] the response of the application
74
+ # @return [Array[Boolean, Hash]] whether the meta tags exists and the evidence hash or nil
75
+ def process_body response
76
+ body = response.body
77
+ # check if the meta tag is include it
78
+ meta_tags = html_elements(body&.split(HEAD_TAG)&.last, META_START_STR)
79
+ meta_tags.each do |tag|
80
+ return true, evidence(META_TYPE, PRAGMA, tag[HTML_PROP]) if meta_cache_tag? tag[HTML_PROP]
81
+ end
82
+ [!meta_tags.empty?, nil]
83
+ end
84
+
85
+ def potential_elements section, element_start
86
+ section.split(element_start)
87
+ end
88
+
89
+ def accepted_http_values
90
+ [/'cache-control'/i, /"cache-control"/i]
91
+ end
92
+
93
+ def accepted_values
94
+ [/'no-cache'/i, /"no-cache"/i, /"no-store"/i, /'no-store'/i, /'cache-control'/i, /"cache-control"/i]
95
+ end
96
+
97
+ # Determine if the given metatag does not have a valid cache-control tag.
98
+ # Meta tags has the option to set http-equiv and content to set the http response header
99
+ # to define for the document
100
+ #
101
+ # @param tag [String] the meta tag
102
+ # @return [Boolean, nil]
103
+ def meta_cache_tag? tag
104
+ # Here we should determine the index of the needed keys
105
+ # http-equiv and content
106
+ http_equiv_idx = tag =~ /http-equiv=/i
107
+ return false unless http_equiv_idx
108
+
109
+ content_idx = tag =~ /content=/i
110
+ return false unless content_idx
111
+
112
+ # determine the value of the http-equiv if it's cache-control
113
+ http_equiv_idx += 11
114
+ is_valid = accepted_http_values.any? { |el| (tag =~ el) == http_equiv_idx }
115
+ return false unless is_valid
116
+
117
+ content_idx += 8
118
+ return false if accepted_values.any? { |value| (tag =~ value) == content_idx }
119
+
120
+ true
121
+ end
122
+
123
+ # This method accepts the violation and transforms it to the proper hash
124
+ # before return in as violation
125
+ #
126
+ # @param type [String] String of Header or META of the type
127
+ # @param name [String] String of either cache-control or pragma
128
+ # @param value [String] String of the violated value
129
+ def evidence type, name, value
130
+ { DATA => { type: type, name: name, value: value }.to_s }
131
+ end
132
+
133
+ # Rebuilds the String value of the Cache-Control Header
134
+ # from the hash build in the Rack::Response
135
+ #
136
+ # @param hsh [Hash]
137
+ # @return [String]
138
+ def cache_control_to_s hsh
139
+ values = []
140
+ hsh.each_pair do |k, v|
141
+ key = k.to_s.tr('_', '-')
142
+ values << if key.to_sym == :extras
143
+ v
144
+ elsif v.is_a?(TrueClass)
145
+ key
146
+ else
147
+ "#{ key }=#{ v }"
148
+ end
149
+ end
150
+ values.join(', ')
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -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 headers contains the required header
13
+ class ClickJacking < HeaderRule
14
+ def rule_id
15
+ 'clickjacking-control-missing'
16
+ end
17
+
18
+ HEADER_KEYS = %w[X-Frame-Options].cs__freeze
19
+ ACCEPTED_VALUES = [/^deny/i, /^sameorigin/i].cs__freeze
20
+ DEFAULT_SAFE = false
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end