contrast-agent 4.12.0 → 4.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +1 -0
  3. data/ext/cs__assess_module/cs__assess_module.c +48 -0
  4. data/ext/cs__assess_module/cs__assess_module.h +7 -0
  5. data/ext/cs__common/cs__common.c +5 -0
  6. data/ext/cs__common/cs__common.h +8 -0
  7. data/ext/cs__contrast_patch/cs__contrast_patch.c +16 -1
  8. data/ext/cs__os_information/cs__os_information.c +31 -0
  9. data/ext/cs__os_information/cs__os_information.h +7 -0
  10. data/ext/cs__os_information/extconf.rb +5 -0
  11. data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
  12. data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
  13. data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
  14. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  15. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
  16. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
  17. data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
  18. data/lib/contrast/agent/assess/policy/trigger_method.rb +45 -110
  19. data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
  20. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
  21. data/lib/contrast/agent/assess/property/tagged.rb +53 -185
  22. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
  23. data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
  24. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +1 -0
  25. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  26. data/lib/contrast/agent/middleware.rb +14 -62
  27. data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
  28. data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
  29. data/lib/contrast/agent/patching/policy/patch.rb +28 -235
  30. data/lib/contrast/agent/patching/policy/patcher.rb +14 -49
  31. data/lib/contrast/agent/reporting/report.rb +21 -0
  32. data/lib/contrast/agent/reporting/reporter.rb +142 -0
  33. data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
  34. data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
  35. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
  36. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
  37. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
  38. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
  39. data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
  40. data/lib/contrast/agent/request.rb +2 -81
  41. data/lib/contrast/agent/request_context.rb +4 -128
  42. data/lib/contrast/agent/request_context_extend.rb +138 -0
  43. data/lib/contrast/agent/request_handler.rb +7 -3
  44. data/lib/contrast/agent/response.rb +2 -73
  45. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +94 -0
  46. data/lib/contrast/agent/static_analysis.rb +5 -3
  47. data/lib/contrast/agent/telemetry.rb +137 -0
  48. data/lib/contrast/agent/telemetry_event.rb +33 -0
  49. data/lib/contrast/agent/thread_watcher.rb +66 -11
  50. data/lib/contrast/agent/version.rb +1 -1
  51. data/lib/contrast/agent.rb +21 -0
  52. data/lib/contrast/api/communication/connection_status.rb +10 -7
  53. data/lib/contrast/api/communication/messaging_queue.rb +37 -3
  54. data/lib/contrast/api/communication/response_processor.rb +15 -8
  55. data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
  56. data/lib/contrast/api/communication/socket.rb +6 -8
  57. data/lib/contrast/api/communication/socket_client.rb +29 -12
  58. data/lib/contrast/api/communication/speedracer.rb +37 -1
  59. data/lib/contrast/api/communication/tcp_socket.rb +4 -3
  60. data/lib/contrast/api/communication/unix_socket.rb +1 -0
  61. data/lib/contrast/api/decorators/finding.rb +45 -0
  62. data/lib/contrast/components/api.rb +90 -0
  63. data/lib/contrast/components/app_context.rb +10 -41
  64. data/lib/contrast/components/app_context_extend.rb +78 -0
  65. data/lib/contrast/components/base.rb +23 -0
  66. data/lib/contrast/components/config.rb +92 -13
  67. data/lib/contrast/components/contrast_service.rb +11 -0
  68. data/lib/contrast/components/sampling.rb +2 -2
  69. data/lib/contrast/config/agent_configuration.rb +1 -1
  70. data/lib/contrast/config/api_configuration.rb +27 -0
  71. data/lib/contrast/config/api_proxy_configuration.rb +14 -0
  72. data/lib/contrast/config/application_configuration.rb +2 -3
  73. data/lib/contrast/config/assess_configuration.rb +3 -3
  74. data/lib/contrast/config/base_configuration.rb +17 -28
  75. data/lib/contrast/config/certification_configuration.rb +15 -0
  76. data/lib/contrast/config/env_variables.rb +18 -0
  77. data/lib/contrast/config/heap_dump_configuration.rb +6 -6
  78. data/lib/contrast/config/inventory_configuration.rb +1 -5
  79. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  80. data/lib/contrast/config/request_audit_configuration.rb +18 -0
  81. data/lib/contrast/config/root_configuration.rb +1 -0
  82. data/lib/contrast/config/ruby_configuration.rb +6 -6
  83. data/lib/contrast/config/service_configuration.rb +2 -2
  84. data/lib/contrast/config.rb +1 -1
  85. data/lib/contrast/configuration.rb +4 -2
  86. data/lib/contrast/extension/assess/array.rb +5 -7
  87. data/lib/contrast/extension/thread.rb +31 -12
  88. data/lib/contrast/framework/manager.rb +22 -44
  89. data/lib/contrast/framework/manager_extend.rb +50 -0
  90. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  91. data/lib/contrast/framework/rails/patch/support.rb +31 -29
  92. data/lib/contrast/framework/rails/railtie.rb +1 -1
  93. data/lib/contrast/framework/sinatra/support.rb +2 -1
  94. data/lib/contrast/logger/application.rb +4 -0
  95. data/lib/contrast/logger/log.rb +8 -103
  96. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  97. data/lib/contrast/utils/assess/property/tagged_utils.rb +165 -0
  98. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  99. data/lib/contrast/utils/assess/tracking_util.rb +20 -15
  100. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  101. data/lib/contrast/utils/class_util.rb +18 -14
  102. data/lib/contrast/utils/exclude_key.rb +20 -0
  103. data/lib/contrast/utils/findings.rb +62 -0
  104. data/lib/contrast/utils/hash_digest.rb +10 -73
  105. data/lib/contrast/utils/hash_digest_extend.rb +86 -0
  106. data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
  107. data/lib/contrast/utils/heap_dump_util.rb +2 -65
  108. data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
  109. data/lib/contrast/utils/io_util.rb +1 -1
  110. data/lib/contrast/utils/log_utils.rb +108 -0
  111. data/lib/contrast/utils/metrics_hash.rb +59 -0
  112. data/lib/contrast/utils/middleware_utils.rb +87 -0
  113. data/lib/contrast/utils/net_http_base.rb +158 -0
  114. data/lib/contrast/utils/object_share.rb +1 -0
  115. data/lib/contrast/utils/os.rb +23 -0
  116. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  117. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  118. data/lib/contrast/utils/request_utils.rb +88 -0
  119. data/lib/contrast/utils/response_utils.rb +97 -0
  120. data/lib/contrast/utils/substitution_utils.rb +167 -0
  121. data/lib/contrast/utils/tag_util.rb +9 -9
  122. data/lib/contrast/utils/telemetry.rb +79 -0
  123. data/lib/contrast/utils/telemetry_client.rb +90 -0
  124. data/lib/contrast/utils/telemetry_identifier.rb +130 -0
  125. data/lib/contrast.rb +18 -0
  126. data/ruby-agent.gemspec +7 -6
  127. data/service_executables/VERSION +1 -1
  128. data/service_executables/linux/contrast-service +0 -0
  129. data/service_executables/mac/contrast-service +0 -0
  130. metadata +69 -22
  131. data/lib/contrast/config/default_value.rb +0 -17
@@ -6,6 +6,7 @@ require 'contrast/agent/assess/policy/source_validation/source_validation'
6
6
  require 'contrast/components/logger'
7
7
  require 'contrast/utils/object_share'
8
8
  require 'contrast/utils/sha256_builder'
9
+ require 'contrast/utils/assess/source_method_utils'
9
10
 
10
11
  module Contrast
11
12
  module Agent
@@ -16,6 +17,7 @@ module Contrast
16
17
  # used in Assess vulnerability detection.
17
18
  module SourceMethod
18
19
  extend Contrast::Components::Logger::InstanceMethods
20
+ extend Contrast::Utils::Assess::SourceMethodUtils
19
21
 
20
22
  PARAMETER_TYPE = 'PARAMETER'
21
23
  PARAMETER_KEY_TYPE = 'PARAMETER_KEY'
@@ -133,15 +135,6 @@ module Contrast
133
135
  !Contrast::Agent::Assess::Tracker.trackable?(key)
134
136
  end
135
137
 
136
- # Safely duplicate the target, or return nil
137
- #
138
- # @param target [Object] the thing to check for duplication
139
- def safe_dup target
140
- target.dup
141
- rescue StandardError => _e
142
- nil
143
- end
144
-
145
138
  # Hash is designed to keep one instance of the string key in it. We need to remove the existing one and
146
139
  # replace it with our new tracked one.
147
140
  def handle_hash_key target, to_replace
@@ -174,68 +167,6 @@ module Contrast
174
167
  properties.build_event(source_node, target, object, ret, args, source_type, source_name)
175
168
  end
176
169
 
177
- # Find the name of the source
178
- #
179
- # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
180
- # event
181
- # @param object [Object] the Object on which the method was invoked
182
- # @param ret [Object] the Return of the invoked method
183
- # @param args [Array<Object>] the Arguments with which the method was invoked
184
- # @return [String, nil] the human readable name of the target to which this source event applies, or nil if
185
- # none provided by the node
186
- def determine_source_name source_node, object, ret, *args
187
- return source_node.get_property('dynamic_source_name') if source_node.type == 'UNTRUSTED_DATABASE'
188
-
189
- source_node_source = source_node.sources[0]
190
- case source_node_source
191
- when nil
192
- nil
193
- when Contrast::Utils::ObjectShare::RETURN_KEY
194
- ret
195
- when Contrast::Utils::ObjectShare::OBJECT_KEY
196
- object
197
- else
198
- args[source_node_source]
199
- end
200
- end
201
-
202
- # Determine if we should analyze this method invocation for a Source or not. We should if we have enough
203
- # information to build the context of this invocation, we're not disabled, and we can't immediately
204
- # determine the invocation was done safely.
205
- #
206
- # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] the policy that applies to the
207
- # method being called
208
- # @param object [Object] the Object on which the method was invoked
209
- # @param ret [Object] the Return of the invoked method
210
- # @param args [Array<Object>] the Arguments with which the method was invoked
211
- # @return [boolean] if the invocation of this method should be analyzed
212
- def analyze? method_policy, object, ret, args
213
- return false unless method_policy&.source_node
214
- return false unless ::Contrast::ASSESS.enabled?
215
- return false unless Contrast::Agent::REQUEST_TRACKER.current&.analyze_request?
216
-
217
- !safe_invocation?(method_policy.source_node, object, ret, args)
218
- end
219
-
220
- # Determine if the method was invoked safely.
221
- #
222
- # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
223
- # event
224
- # @param _object [Object] the Object on which the method was invoked
225
- # @param _ret [Object] the Return of the invoked method
226
- # @param args [Array<Object>] the Arguments with which the method was invoked
227
- # @return [boolean] if the invocation of this method was safe
228
- def safe_invocation? source_node, _object, _ret, args
229
- # According the the Rack Specification https://github.com/rack/rack/blob/master/SPEC.rdoc, any header
230
- # from the Request will start with HTTP_. As such, only Headers with that key should be considered for
231
- # tracking, as the others have come from the Framework or Middleware stashing in the ENV. Rails, for
232
- # instance, uses action_dispatch. to store several values. Technically, you can't call
233
- # Rack::Request#get_header without a parameter, and that parameter should be a String, but trust no one.
234
- source_node.id == 'Assess:Source:Rack::Request::Env#get_header' &&
235
- args&.any? &&
236
- !args[0].to_s.start_with?('HTTP_')
237
- end
238
-
239
170
  # Find the literal target of the propagation
240
171
  #
241
172
  # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
@@ -6,6 +6,8 @@ require 'contrast/agent/assess/policy/trigger_validation/trigger_validation'
6
6
  require 'contrast/components/logger'
7
7
  require 'contrast/utils/object_share'
8
8
  require 'contrast/utils/sha256_builder'
9
+ require 'contrast/utils/assess/trigger_method_utils'
10
+ require 'contrast/agent/reporting/reporting_utilities/reporting_storage'
9
11
 
10
12
  module Contrast
11
13
  module Agent
@@ -15,8 +17,9 @@ module Contrast
15
17
  # Contrast::Agent::Assess::Policy::TriggerNode class. Each such method will call to this module just after
16
18
  # invocation in order to determine if the call was done safely. In those cases where it was not, a Finding
17
19
  # report is issued to the Service.
18
- module TriggerMethod
20
+ module TriggerMethod # rubocop:disable Metrics/ModuleLength
19
21
  extend Contrast::Components::Logger::InstanceMethods
22
+ extend Contrast::Utils::Assess::TriggerMethodUtils
20
23
 
21
24
  # The level of TeamServer compliance our traces meet when in the abnormal condition of being dataflow rules
22
25
  # without routes.
@@ -50,6 +53,14 @@ module Contrast
50
53
  apply_trigger(trigger_node, source, object, ret, *args)
51
54
  end
52
55
 
56
+ def append_to_finding finding, trigger_node, source, object, ret, request, *args
57
+ finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id)
58
+ finding.version = determine_compliance_version(finding)
59
+ append_events(finding, trigger_node, source, object, ret, args)
60
+ append_route(finding, request)
61
+ append_hash(finding, source, request)
62
+ end
63
+
53
64
  # This converts the source of the finding, and the events leading up to it into a Finding
54
65
  #
55
66
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
@@ -61,18 +72,21 @@ module Contrast
61
72
  # @return [Contrast::Api::Dtm::Finding, nil] the Contrast::Api::Dtm::Finding to send to TeamServer or
62
73
  # nil if conditions were not met
63
74
  def build_finding trigger_node, source, object, ret, *args
75
+ content_type = Contrast::Agent::REQUEST_TRACKER.current&.response&.content_type
76
+
77
+ if content_type.nil? && trigger_node.collectable?
78
+ Contrast::Agent::FINDINGS.collect_finding trigger_node, source, object, ret, *args
79
+ return
80
+ end
81
+
64
82
  return unless Contrast::Agent::Assess::Policy::TriggerValidation.valid?(trigger_node, object, ret, args)
65
83
 
66
84
  request = find_request(source)
67
85
  return unless reportable?(request&.env)
68
86
 
87
+ handle_new_finding trigger_node, source, object, ret, request, *args
69
88
  finding = Contrast::Api::Dtm::Finding.new
70
- finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id)
71
-
72
- append_events(finding, trigger_node, source, object, ret, args)
73
- append_route(finding, request)
74
- append_hash(finding, source, request)
75
- finding.version = determine_compliance_version(finding)
89
+ append_to_finding finding, trigger_node, source, object, ret, request, *args
76
90
  logger.trace('Finding created', node_id: trigger_node.id, source_id: source.__id__,
77
91
  rule: trigger_node.rule_id)
78
92
  report_finding(finding, request)
@@ -84,6 +98,8 @@ module Contrast
84
98
  # activity message does not exist, b/c we're invoked outside of a request context, build an activity and
85
99
  # immediately report it with the finding.
86
100
  #
101
+ # TODO: RUBY-1351
102
+ #
87
103
  # @param finding [Contrast::Api::Dtm::Finding] the Finding to report.
88
104
  # @param request [Contrast::Agent::Request] our wrapper around the Rack::Request.
89
105
  def report_finding finding, request = nil
@@ -106,66 +122,37 @@ module Contrast
106
122
  Contrast::Agent.messaging_queue.send_event_eventually(activity)
107
123
  end
108
124
 
109
- private
125
+ def handle_new_finding trigger_node, source, object, ret, request, *args
126
+ return unless Contrast::Agent::Reporter.enabled?
110
127
 
111
- # A request is reportable if it is not from ActionController::Live
112
- #
113
- # @param env [Hash] the env of the Request
114
- # @return [Boolean]
115
- def reportable? env
116
- !(defined?(ActionController::Live) &&
117
- env &&
118
- env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live))
128
+ new_finding_and_reporting trigger_node, source, object, ret, request, *args
119
129
  end
120
130
 
121
- # Find the request for this finding. This assumes, for now, that if there is an active request, then that
122
- # is the request to report. Otherwise, we'll use the first request found in the events of the
123
- # source_object.
124
- #
125
- # @param source [Object,nil] some Object used as the source of a trigger event
126
- # @return [Contrast::Agent::Request,nil] the request from which the dataflow on the request originated.
127
- def find_request source
128
- return Contrast::Agent::REQUEST_TRACKER.current.request if Contrast::Agent::REQUEST_TRACKER.current
129
- return unless (properties = Contrast::Agent::Assess::Tracker.properties(source))
130
-
131
- properties.events.each do |event|
132
- next unless event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent)
133
-
134
- return event.request if event.request
135
- end
136
- nil
131
+ def new_finding_and_reporting trigger_node, source, object, ret, request, *args
132
+ # sent to reporter
133
+ # here we will generate new type of finding
134
+ ruby_finding = Contrast::Agent::Reporting::Finding.new trigger_node.rule_id
135
+ ruby_finding.attach_data trigger_node, source, object, ret, request, *args
136
+ hash_code = Contrast::Utils::HashDigest.generate_event_hash(ruby_finding, source, request)
137
+ ruby_finding.hash_code = hash_code
138
+ # save the current finding
139
+ Contrast::Agent::Reporting::ReportingStorage[hash_code] = ruby_finding
140
+
141
+ new_preflight = Contrast::Agent::Reporting::Preflight.new
142
+ new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new
143
+ new_preflight_message.routes << request
144
+ new_preflight_message.hash_code = hash_code
145
+ new_preflight_message.data = "#{ trigger_node.rule_id },#{ hash_code }"
146
+ new_preflight.messages << new_preflight_message
147
+ Contrast::Agent.reporter_queue.send_event_immediately(new_preflight)
137
148
  end
138
149
 
150
+ private
151
+
139
152
  def settings
140
153
  Contrast::Agent::FeatureState.instance
141
154
  end
142
155
 
143
- # This is our method that actually checks the taint on the object our trigger_node targets.
144
- #
145
- # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
146
- # trigger event
147
- # @param source [Object] the source of the Trigger Event
148
- # @param object [Object] the Object on which the method was invoked
149
- # @param ret [Object] the Return of the invoked method
150
- # @param args [Array<Object>] the Arguments with which the method was invoked
151
- def apply_trigger trigger_node, source, object, ret, *args
152
- return unless trigger_node
153
- return if trigger_node.rule_disabled?
154
- return if trigger_node.dataflow? && source.nil?
155
-
156
- if trigger_node.regexp_rule?
157
- apply_regexp_rule(trigger_node, source, object, ret, *args)
158
- elsif trigger_node.custom_trigger?
159
- trigger_node.apply_custom_trigger(trigger_node, source, object, ret, *args)
160
- elsif trigger_node.dataflow?
161
- apply_dataflow_rule(trigger_node, source, object, ret, *args)
162
- else # trigger rule - just calling the method is dangerous
163
- build_finding(trigger_node, source, object, ret, *args)
164
- end
165
- rescue StandardError => e
166
- logger.warn('Unable to apply trigger', e, node_id: trigger_node.id)
167
- end
168
-
169
156
  # Given the marker from the trigger_node (the pointer indicating the entity from which the taint
170
157
  # originated), return the entity on which this trigger needs to operate.
171
158
  #
@@ -199,58 +186,6 @@ module Contrast
199
186
  end
200
187
  end
201
188
 
202
- # This is our method that actually checks the taint on the object our trigger_node targets for our Regexp
203
- # based rules.
204
- #
205
- # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
206
- # trigger event
207
- # @param source [Object] the source of the Trigger Event
208
- # @param object [Object] the Object on which the method was invoked
209
- # @param ret [Object] the Return of the invoked method
210
- # @param args [Array<Object>] the Arguments with which the method was invoked
211
- def apply_regexp_rule trigger_node, source, object, ret, *args
212
- return unless source.is_a?(String)
213
- return if trigger_node.good_value && source.match?(trigger_node.good_value)
214
- return if trigger_node.bad_value && source !~ trigger_node.bad_value
215
-
216
- build_finding(trigger_node, source, object, ret, *args)
217
- end
218
-
219
- # This is our method that actually checks the taint on the object our trigger_node targets for our Dataflow
220
- # based rules.
221
- #
222
- # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
223
- # trigger event
224
- # @param source [Object] the source of the Trigger Event
225
- # @param object [Object] the Object on which the method was invoked
226
- # @param ret [Object] the Return of the invoked method
227
- # @param args [Array<Object>] the Arguments with which the method was invoked
228
- def apply_dataflow_rule trigger_node, source, object, ret, *args
229
- return unless source
230
-
231
- if Contrast::Agent::Assess::Tracker.trackable?(source)
232
- return unless Contrast::Agent::Assess::Tracker.tracked?(source)
233
- return unless trigger_node.violated?(source)
234
-
235
- build_finding(trigger_node, source, object, ret, *args)
236
- elsif Contrast::Utils::DuckUtils.iterable_hash?(source)
237
- source.each_pair do |key, value|
238
- apply_dataflow_rule(trigger_node, key, object, ret, *args)
239
- apply_dataflow_rule(trigger_node, value, object, ret, *args)
240
- end
241
- elsif Contrast::Utils::DuckUtils.iterable_enumerable?(source)
242
- source.each do |value|
243
- apply_dataflow_rule(trigger_node, value, object, ret, *args)
244
- end
245
- else
246
- logger.debug('Trigger source is untrackable. Unable to inspect.',
247
- node_id: trigger_node.id,
248
- source_id: source.__id__,
249
- source_type: source.cs__class.cs__name,
250
- frozen: source.cs__frozen?)
251
- end
252
- end
253
-
254
189
  def append_events finding, trigger_node, source, object, ret, args
255
190
  append_from_source(finding, source)
256
191
  finding.events << Contrast::Agent::Assess::Events::EventFactory.build(trigger_node, source, object, ret,
@@ -22,6 +22,10 @@ module Contrast
22
22
  JSON_RULE_NAME = 'name'
23
23
  JSON_CUSTOM_PATCH = 'custom_patch'
24
24
 
25
+ # Our list with rules to be collected and reported back when we have response
26
+ # from the application. Some rules rely on Content-Type validation.
27
+ COLLECTABLE_RULES = %w[reflected-xss].cs__freeze
28
+
25
29
  attr_reader :rule_id, :required_tags, :disallowed_tags, :good_value, :bad_value
26
30
 
27
31
  def initialize trigger_hash = {}, rule_hash = {}
@@ -67,6 +71,10 @@ module Contrast
67
71
  :TYPE_METHOD
68
72
  end
69
73
 
74
+ def collectable?
75
+ COLLECTABLE_RULES.include?(rule_id)
76
+ end
77
+
70
78
  def rule_disabled?
71
79
  ::Contrast::ASSESS.rule_disabled?(rule_id)
72
80
  end
@@ -160,8 +168,8 @@ module Contrast
160
168
  @disallowed_tags << LIMITED_CHARS
161
169
  @disallowed_tags << CUSTOM_ENCODED
162
170
  @disallowed_tags << CUSTOM_VALIDATED
163
- @disallowed_tags << ENCODER_START + loud_name
164
- @disallowed_tags << VALIDATOR_START + loud_name
171
+ @disallowed_tags << (ENCODER_START + loud_name)
172
+ @disallowed_tags << (VALIDATOR_START + loud_name)
165
173
  end
166
174
 
167
175
  def validate_rule_tags tags
@@ -200,14 +208,14 @@ module Contrast
200
208
  satisfied = tags_at.any? && required_tags.all? { |tag| tags_at.any? { |found| found.label == tag } }
201
209
  # if this range matches all the required tags and we're already
202
210
  # chunking, meaning the previous range matched, do nothing
203
- if satisfied && chunking
204
- start_range += 1
205
- next
206
- end
207
211
 
208
212
  # if we are satisfied and we were not chunking, this represents
209
213
  # the start of the next range, so create a new entry.
210
214
  if satisfied
215
+ if chunking
216
+ start_range += 1
217
+ next
218
+ end
211
219
  ranges << Contrast::Agent::Assess::Tag.new('required', 0, start_range)
212
220
  chunking = true
213
221
  # if we are chunking and not satisfied, this represents the end
@@ -18,7 +18,7 @@ module Contrast
18
18
  # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/rules/dataflow/reflected_xss.md
19
19
  def self.valid? _patcher, _object, _ret, _args
20
20
  content_type = Contrast::Agent::REQUEST_TRACKER.current&.response&.content_type
21
- return true unless content_type
21
+ return false unless content_type
22
22
 
23
23
  content_type = content_type.downcase
24
24
  SAFE_CONTENT_TYPES.none? { |safe_type| content_type.index(safe_type) }
@@ -5,6 +5,7 @@ require 'contrast/agent/assess/tag'
5
5
  require 'contrast/utils/object_share'
6
6
  require 'contrast/utils/string_utils'
7
7
  require 'contrast/utils/tag_util'
8
+ require 'contrast/utils/assess/property/tagged_utils'
8
9
 
9
10
  module Contrast
10
11
  module Agent
@@ -13,6 +14,7 @@ module Contrast
13
14
  # This module serves to hold the functionality required for the
14
15
  # management of our dataflow tags.
15
16
  module Tagged
17
+ include Contrast::Utils::Assess::TaggedUtils
16
18
  # Is any tag present?
17
19
  # Creating Tags is expensive and we check for Tags all the time on
18
20
  # untracked things. ALWAYS!!! call this method before checking if an
@@ -22,157 +24,6 @@ module Contrast
22
24
  instance_variable_defined?(:@_tags) && tags.any?
23
25
  end
24
26
 
25
- # Is the given tag present?
26
- # Used in testing, so found by `be_tagged`, if you're grepping for it
27
- #
28
- # @param label [Symbol] the tag to check for
29
- # @return [Boolean]
30
- def tagged? label
31
- tracked? && tags.key?(label)
32
- end
33
-
34
- # Similar to #tracked?, but limited to a given range.
35
- #
36
- # @param start [Integer] the inclusive start index to check.
37
- # @param finish [Integer] the exclusive end index to check.
38
- # @return [Boolean]
39
- def any_tags_between? start, finish
40
- return false unless tracked?
41
-
42
- tags.each_value do |tag_array|
43
- return true if tag_array.any? { |tag| tag.overlaps?(start, finish) }
44
- end
45
- false
46
- end
47
-
48
- # Find all of the ranges that span a given index. This is used
49
- # in propagation when we need to shift tags about. For instance, in
50
- # the append method when we need to see if any tag at the end needs
51
- # to be expanded out to the size of the new String.
52
- #
53
- # Note: Tags do not know their key, so this is only the range covered
54
- #
55
- # @param idx [Integer] the index to check for tags
56
- # @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
57
- # covering the given index. This is only the ranges, not the names.
58
- def tags_at idx
59
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
60
-
61
- at = []
62
- tags.each_value do |tag_array|
63
- tag_array.each do |tag|
64
- if tag.covers?(idx)
65
- at << tag
66
- elsif tag.above?(idx)
67
- break
68
- end
69
- end
70
- end
71
- at
72
- end
73
-
74
- # given a range, select all tags in that range the selected tags are
75
- # shifted such that the start index of the new tag (0) aligns with
76
- # the given start index in the range
77
- #
78
- # current tags: 5-15
79
- # range : 5-10
80
- # result : 0-05
81
- #
82
- # Note that we disable Lint/DuplicateBranch in this branch in order
83
- # list out all tag range cases in the proper order to make this
84
- # easier to understand
85
- #
86
- # @param range [Range] the span to check, inclusive to exclusive
87
- # @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of
88
- # key to tags
89
- def tags_at_range range
90
- return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
91
-
92
- at = Hash.new { |h, k| h[k] = [] }
93
- tags.each_pair do |key, value|
94
- add = nil
95
- value.each do |tag|
96
- within_range = resize_to_range(tag, range)
97
- if within_range
98
- add ||= []
99
- add << within_range
100
- end
101
- end
102
- next unless add&.any?
103
-
104
- at[key] = add
105
- end
106
- at
107
- end
108
-
109
- # Given a tag name and range object, add a new tag to this
110
- # collection. If the given range touches an existing tag,
111
- # we'll combine the two, adjusting the existing one and
112
- # dropping this new one.
113
- #
114
- # @param label [String] the name of the tag
115
- # @param range [Range] the Range that the tag covers, inclusive to
116
- # exclusive
117
- def add_tag label, range
118
- length = range.end - range.begin
119
- tag = Contrast::Agent::Assess::Tag.new(label, length, range.begin)
120
- existing = fetch_tag(label)
121
- tags[label] = Contrast::Utils::TagUtil.ordered_merge(existing, tag)
122
- end
123
-
124
- def set_tags label, tag_ranges
125
- tags[label] = tag_ranges
126
- end
127
-
128
- # Returns a list of all current tags.
129
- #
130
- # @return [Hash<Contrast::Agent::Assess::Tag>]
131
- def get_tags # rubocop:disable Naming/AccessorMethodName
132
- return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
133
-
134
- tags
135
- end
136
-
137
- # We'll use this as a helper method to retrieve tags from the hash.
138
- # Because the hash auto-populates an empty array when we try to
139
- # access a tag in it, we cannot use the [] method without side
140
- # effect. To get around this, we'll use a fetch work around.
141
- #
142
- # @param label [Symbol] the label to look up
143
- # @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
144
- # that label
145
- def fetch_tag label
146
- get_tags.fetch(label, nil) if tracked?
147
- end
148
-
149
- # Remove all tags with a given label
150
- def delete_tags label
151
- tags.delete(label) if tracked?
152
- end
153
-
154
- # Reset the tag hash
155
- def clear_tags
156
- tags.clear if tracked?
157
- end
158
-
159
- # Returns a list of all current tag labels, most likely to be used for
160
- # a splat operation
161
- def tag_keys
162
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
163
-
164
- tags.keys
165
- end
166
-
167
- # Calls merge to combine touching or overlapping tags
168
- # Deletes empty tags
169
- def cleanup_tags
170
- return unless tracked?
171
-
172
- Contrast::Utils::TagUtil.merge_tags(tags)
173
- tags.delete_if { |_, value| value.empty? }
174
- end
175
-
176
27
  # Remove all tags within the given ranges.
177
28
  # This does not delete an entire tag if part of that tag is
178
29
  # outside this range, meaning we may reduce sizes of tags
@@ -228,6 +79,8 @@ module Contrast
228
79
  end
229
80
 
230
81
  # Remove the tag ranges covering the given range
82
+ # and appends any trailing value that might
83
+ # exist after removal of range
231
84
  def remove_tags range
232
85
  return unless tracked?
233
86
 
@@ -238,19 +91,7 @@ module Contrast
238
91
  value.each do |tag|
239
92
  comparison = tag.compare_range(range.begin, range.end)
240
93
  # ABOVE and BELOW are not affected by this check
241
- case comparison
242
- when Contrast::Agent::Assess::Tag::LOW_SPAN
243
- tag.update_end(range.begin)
244
- when Contrast::Agent::Assess::Tag::WITHIN
245
- remove << tag
246
- when Contrast::Agent::Assess::Tag::WITHOUT
247
- new_tag = tag.clone
248
- new_tag.update_start(range.end)
249
- add << new_tag
250
- tag.update_end(range.begin)
251
- when Contrast::Agent::Assess::Tag::HIGH_SPAN
252
- tag.update_start(range.end)
253
- end
94
+ tags_remove_comparison comparison, tag, remove, add, range
254
95
  end
255
96
  value.delete_if { |tag| remove.include?(tag) }
256
97
  Contrast::Utils::TagUtil.ordered_merge(value, add)
@@ -259,6 +100,29 @@ module Contrast
259
100
  full_delete.each { |key| tags.delete(key) }
260
101
  end
261
102
 
103
+ # This method is for the tags comparison
104
+ # the idea is to move the whole case here
105
+ # @param comparison [String] indicates type of removal is to occur
106
+ # @param tag Contrast::Agent::Assess::Tag
107
+ # @param remove [String] holds removed Tag if exists
108
+ # @param add [String] holds trailing Tag if exists
109
+ # @param range [Range] start and stop for idx for removal
110
+ def tags_remove_comparison comparison, tag, remove, add, range
111
+ case comparison
112
+ when Contrast::Agent::Assess::Tag::LOW_SPAN
113
+ tag.update_end(range.begin)
114
+ when Contrast::Agent::Assess::Tag::WITHIN
115
+ remove << tag
116
+ when Contrast::Agent::Assess::Tag::WITHOUT
117
+ new_tag = tag.clone
118
+ new_tag.update_start(range.end)
119
+ add << new_tag
120
+ tag.update_end(range.begin)
121
+ when Contrast::Agent::Assess::Tag::HIGH_SPAN
122
+ tag.update_start(range.end)
123
+ end
124
+ end
125
+
262
126
  # Shift the tag ranges covering the given range
263
127
  # We assume this is for a deletion, meaning we
264
128
  # have to move tags to the left
@@ -302,32 +166,36 @@ module Contrast
302
166
  comparison = tag.compare_range(range.begin, range.end)
303
167
  length = range.end - range.begin
304
168
  # BELOW is not affected by this check
305
- case comparison
306
- # part of the tag is being inserted on
307
- when Contrast::Agent::Assess::Tag::LOW_SPAN
308
- new_tag = tag.clone
309
- new_tag.update_start(range.begin)
310
- new_tag.shift(length)
311
- add << new_tag
312
- tag.update_end(range.begin)
313
- # the tag exists in the inserted range. it is partially shifted
314
- when Contrast::Agent::Assess::Tag::WITHIN
315
- tag.shift(length)
316
- # the tag spans the range. leave the part below alone
317
- when Contrast::Agent::Assess::Tag::WITHOUT # rubocop:disable Lint/DuplicateBranch
318
- new_tag = tag.clone
319
- new_tag.update_start(range.begin)
320
- new_tag.shift(length)
321
- add << new_tag
322
- tag.update_end(range.begin)
323
- when Contrast::Agent::Assess::Tag::HIGH_SPAN, Contrast::Agent::Assess::Tag::ABOVE # rubocop:disable Lint/DuplicateBranch
324
- tag.shift(length)
325
- end
169
+ shift_tags_comparison comparison, add, tag, length, range
326
170
  end
327
171
  Contrast::Utils::TagUtil.ordered_merge(value, add)
328
172
  end
329
173
  end
330
174
 
175
+ def shift_tags_comparison comparison, add, tag, length, range
176
+ case comparison
177
+ # part of the tag is being inserted on
178
+ when Contrast::Agent::Assess::Tag::LOW_SPAN
179
+ new_tag = tag.clone
180
+ new_tag.update_start(range.begin)
181
+ new_tag.shift(length)
182
+ add << new_tag
183
+ tag.update_end(range.begin)
184
+ # the tag exists in the inserted range. it is partially shifted
185
+ when Contrast::Agent::Assess::Tag::WITHIN
186
+ tag.shift(length)
187
+ # the tag spans the range. leave the part below alone
188
+ when Contrast::Agent::Assess::Tag::WITHOUT # rubocop:disable Lint/DuplicateBranch
189
+ new_tag = tag.clone
190
+ new_tag.update_start(range.begin)
191
+ new_tag.shift(length)
192
+ add << new_tag
193
+ tag.update_end(range.begin)
194
+ when Contrast::Agent::Assess::Tag::HIGH_SPAN, Contrast::Agent::Assess::Tag::ABOVE # rubocop:disable Lint/DuplicateBranch
195
+ tag.shift(length)
196
+ end
197
+ end
198
+
331
199
  private
332
200
 
333
201
  # Because of the auto-fill thing, we should not allow direct access to