contrast-agent 3.14.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.c +18 -15
  3. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.h +1 -0
  4. data/ext/cs__assess_string/cs__assess_string.c +24 -25
  5. data/ext/cs__assess_string/cs__assess_string.h +3 -1
  6. data/ext/cs__common/cs__common.c +4 -2
  7. data/ext/cs__common/cs__common.h +1 -1
  8. data/lib/contrast.rb +1 -1
  9. data/lib/contrast/agent.rb +4 -12
  10. data/lib/contrast/agent/assess.rb +1 -0
  11. data/lib/contrast/agent/assess/contrast_event.rb +143 -79
  12. data/lib/contrast/agent/assess/events/source_event.rb +1 -1
  13. data/lib/contrast/agent/assess/finalizers/freeze.rb +3 -1
  14. data/lib/contrast/agent/assess/finalizers/hash.rb +45 -1
  15. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +10 -3
  16. data/lib/contrast/agent/assess/policy/patcher.rb +1 -1
  17. data/lib/contrast/agent/assess/policy/policy.rb +0 -2
  18. data/lib/contrast/agent/assess/policy/policy_node.rb +15 -10
  19. data/lib/contrast/agent/assess/policy/policy_scanner.rb +19 -3
  20. data/lib/contrast/agent/assess/policy/preshift.rb +7 -11
  21. data/lib/contrast/agent/assess/policy/propagation_method.rb +50 -33
  22. data/lib/contrast/agent/assess/policy/propagator/append.rb +8 -5
  23. data/lib/contrast/agent/assess/policy/propagator/base.rb +1 -1
  24. data/lib/contrast/agent/assess/policy/propagator/center.rb +9 -5
  25. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +5 -3
  26. data/lib/contrast/agent/assess/policy/propagator/insert.rb +7 -4
  27. data/lib/contrast/agent/assess/policy/propagator/keep.rb +4 -1
  28. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +4 -7
  29. data/lib/contrast/agent/assess/policy/propagator/next.rb +7 -5
  30. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +8 -5
  31. data/lib/contrast/agent/assess/policy/propagator/remove.rb +8 -4
  32. data/lib/contrast/agent/assess/policy/propagator/replace.rb +5 -2
  33. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +7 -5
  34. data/lib/contrast/agent/assess/policy/propagator/select.rb +13 -7
  35. data/lib/contrast/agent/assess/policy/propagator/splat.rb +10 -9
  36. data/lib/contrast/agent/assess/policy/propagator/split.rb +24 -19
  37. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +47 -31
  38. data/lib/contrast/agent/assess/policy/propagator/trim.rb +11 -5
  39. data/lib/contrast/agent/assess/policy/source_method.rb +85 -58
  40. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +16 -12
  41. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  42. data/lib/contrast/agent/assess/policy/trigger_method.rb +76 -28
  43. data/lib/contrast/agent/assess/policy/trigger_node.rb +38 -43
  44. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +2 -1
  45. data/lib/contrast/agent/assess/properties.rb +2 -0
  46. data/lib/contrast/agent/assess/property/evented.rb +5 -18
  47. data/lib/contrast/agent/assess/property/tagged.rb +9 -3
  48. data/lib/contrast/agent/assess/property/updated.rb +131 -0
  49. data/lib/contrast/agent/assess/rule/provider/hardcoded_key.rb +58 -5
  50. data/lib/contrast/agent/assess/rule/provider/hardcoded_password.rb +23 -8
  51. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +83 -14
  52. data/lib/contrast/agent/assess/tag.rb +1 -1
  53. data/lib/contrast/agent/assess/tracker.rb +66 -0
  54. data/lib/contrast/agent/at_exit_hook.rb +5 -5
  55. data/lib/contrast/agent/class_reopener.rb +7 -5
  56. data/lib/contrast/agent/inventory.rb +15 -0
  57. data/lib/contrast/agent/inventory/dependencies.rb +50 -0
  58. data/lib/contrast/agent/inventory/dependency_analysis.rb +37 -0
  59. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +104 -0
  60. data/lib/contrast/agent/inventory/gemfile_digest_cache.rb +38 -0
  61. data/lib/contrast/agent/middleware.rb +1 -3
  62. data/lib/contrast/agent/patching/policy/after_load_patch.rb +5 -5
  63. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +20 -20
  64. data/lib/contrast/agent/patching/policy/module_policy.rb +10 -10
  65. data/lib/contrast/agent/patching/policy/patch.rb +6 -0
  66. data/lib/contrast/agent/patching/policy/patcher.rb +13 -22
  67. data/lib/contrast/agent/patching/policy/policy.rb +17 -6
  68. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +3 -5
  69. data/lib/contrast/agent/protect/policy/applies_path_traversal_rule.rb +4 -3
  70. data/lib/contrast/agent/protect/policy/applies_xxe_rule.rb +1 -1
  71. data/lib/contrast/agent/protect/rule/cmd_injection.rb +9 -25
  72. data/lib/contrast/agent/protect/rule/no_sqli/mongo_no_sql_scanner.rb +1 -0
  73. data/lib/contrast/agent/request.rb +34 -34
  74. data/lib/contrast/agent/request_handler.rb +1 -1
  75. data/lib/contrast/agent/response.rb +17 -6
  76. data/lib/contrast/agent/rewriter.rb +1 -3
  77. data/lib/contrast/agent/scope.rb +59 -53
  78. data/lib/contrast/agent/static_analysis.rb +7 -7
  79. data/lib/contrast/agent/tracepoint_hook.rb +1 -1
  80. data/lib/contrast/agent/version.rb +1 -1
  81. data/lib/contrast/api/communication/messaging_queue.rb +1 -4
  82. data/lib/contrast/api/communication/socket_client.rb +36 -1
  83. data/lib/contrast/api/decorators.rb +3 -0
  84. data/lib/contrast/api/decorators/address.rb +13 -14
  85. data/lib/contrast/api/decorators/application_update.rb +2 -4
  86. data/lib/contrast/api/decorators/library.rb +53 -0
  87. data/lib/contrast/api/decorators/library_usage_update.rb +30 -0
  88. data/lib/contrast/api/decorators/message.rb +1 -0
  89. data/lib/contrast/api/decorators/trace_event.rb +25 -23
  90. data/lib/contrast/common_agent_configuration.rb +2 -1
  91. data/lib/contrast/components/agent.rb +6 -5
  92. data/lib/contrast/components/app_context.rb +49 -38
  93. data/lib/contrast/components/config.rb +30 -48
  94. data/lib/contrast/components/contrast_service.rb +9 -9
  95. data/lib/contrast/components/interface.rb +25 -3
  96. data/lib/contrast/components/inventory.rb +6 -1
  97. data/lib/contrast/components/scope.rb +49 -6
  98. data/lib/contrast/components/settings.rb +23 -23
  99. data/lib/contrast/config/application_configuration.rb +5 -2
  100. data/lib/contrast/config/inventory_configuration.rb +2 -2
  101. data/lib/contrast/config/service_configuration.rb +8 -0
  102. data/lib/contrast/configuration.rb +88 -47
  103. data/lib/contrast/extension/assess.rb +0 -2
  104. data/lib/contrast/extension/assess/array.rb +15 -8
  105. data/lib/contrast/extension/assess/erb.rb +11 -3
  106. data/lib/contrast/extension/assess/eval_trigger.rb +6 -6
  107. data/lib/contrast/extension/assess/exec_trigger.rb +1 -4
  108. data/lib/contrast/extension/assess/fiber.rb +12 -12
  109. data/lib/contrast/extension/assess/hash.rb +5 -6
  110. data/lib/contrast/extension/assess/kernel.rb +28 -23
  111. data/lib/contrast/extension/assess/marshal.rb +11 -6
  112. data/lib/contrast/extension/assess/regexp.rb +8 -7
  113. data/lib/contrast/extension/assess/string.rb +21 -21
  114. data/lib/contrast/extension/protect/kernel.rb +3 -3
  115. data/lib/contrast/framework/base_support.rb +1 -1
  116. data/lib/contrast/framework/manager.rb +3 -3
  117. data/lib/contrast/framework/rack/patch/session_cookie.rb +22 -28
  118. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +13 -13
  119. data/lib/contrast/framework/rails/patch/assess_configuration.rb +5 -11
  120. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +10 -10
  121. data/lib/contrast/framework/rails/patch/support.rb +1 -1
  122. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +11 -11
  123. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +12 -12
  124. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +3 -3
  125. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +12 -12
  126. data/lib/contrast/framework/rails/support.rb +5 -0
  127. data/lib/contrast/framework/sinatra/patch/base.rb +11 -11
  128. data/lib/contrast/framework/sinatra/support.rb +4 -4
  129. data/lib/contrast/logger/application.rb +11 -3
  130. data/lib/contrast/logger/log.rb +7 -2
  131. data/lib/contrast/utils/assess/tracking_util.rb +48 -3
  132. data/lib/contrast/utils/duck_utils.rb +0 -10
  133. data/lib/contrast/utils/env_configuration_item.rb +2 -1
  134. data/lib/contrast/utils/invalid_configuration_util.rb +20 -21
  135. data/lib/contrast/utils/inventory_util.rb +0 -7
  136. data/lib/contrast/utils/sha256_builder.rb +0 -12
  137. data/lib/contrast/utils/string_utils.rb +10 -5
  138. data/resources/assess/policy.json +31 -22
  139. data/ruby-agent.gemspec +21 -18
  140. data/service_executables/VERSION +1 -1
  141. data/service_executables/linux/contrast-service +0 -0
  142. data/service_executables/mac/contrast-service +0 -0
  143. metadata +71 -30
  144. data/lib/contrast/agent/assess/finalizers/finalize.rb +0 -21
  145. data/lib/contrast/extension/assess/assess_extension.rb +0 -145
  146. data/lib/contrast/utils/boolean_util.rb +0 -30
  147. data/lib/contrast/utils/freeze_util.rb +0 -32
  148. data/lib/contrast/utils/gemfile_reader.rb +0 -193
@@ -39,7 +39,7 @@ module Contrast
39
39
  def process context, trigger_node, object, ret, *args
40
40
  args.each do |arg|
41
41
  next unless arg.cs__is_a?(String) || arg.cs__is_a?(Symbol)
42
- next unless arg.cs__tracked?
42
+ next unless Contrast::Agent::Assess::Tracker.tracked?(arg)
43
43
  next unless trigger_node.violated?(arg)
44
44
 
45
45
  Contrast::Agent::Assess::Policy::TriggerMethod.build_finding(
@@ -20,10 +20,31 @@ module Contrast
20
20
  include Contrast::Components::Interface
21
21
  access_component :analysis, :logging
22
22
 
23
+ # The level of TeamServer compliance our traces meet when in the
24
+ # abnormal condition of being dataflow rules without routes
25
+ MINIMUM_FINDING_VERSION = 3
23
26
  # The level of TeamServer compliance our traces meet
24
- CURRENT_FINDING_VERSION = 2
27
+ CURRENT_FINDING_VERSION = 4
25
28
 
26
29
  class << self
30
+ # Append the given finding to the given context to be reported when
31
+ # the Context's activity is sent to the Service or, in the absence
32
+ # of that Context, generate an Activity and queue it manually
33
+ # @param finding [Contrast::Api::Dtm::Finding]
34
+ def report_finding finding
35
+ context = Contrast::Agent::REQUEST_TRACKER.current
36
+ if context
37
+ context.activity.findings << finding
38
+ else
39
+ activity = Contrast::Api::Dtm::Activity.new
40
+ activity.findings << finding
41
+
42
+ Contrast::Agent.messaging_queue.send_event_eventually(activity)
43
+ end
44
+ logger.debug('Finding reported',
45
+ rule: finding.rule_id)
46
+ end
47
+
27
48
  # This is called from within our woven proc. It will be called as if it
28
49
  # were inline in the Rack application.
29
50
  #
@@ -66,8 +87,8 @@ module Contrast
66
87
  # This converts the source of the finding, and the events leading
67
88
  # up to it into a Finding
68
89
  #
69
- # @param context [Contrast::Utils::ThreadTracker] the current request
70
- # context
90
+ # @param context [Contrast::Agent::RequestContext] the current
91
+ # request context
71
92
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode]
72
93
  # the node to direct applying this trigger event
73
94
  # @param source [Object] the source of the Trigger Event
@@ -89,18 +110,17 @@ module Contrast
89
110
 
90
111
  finding = Contrast::Api::Dtm::Finding.new
91
112
  finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id)
92
- finding.version = CURRENT_FINDING_VERSION
93
-
94
113
  build_from_source(finding, source)
95
114
  trigger_event = Contrast::Agent::Assess::Events::EventFactory.build(trigger_node, source, object, ret, args).to_dtm_event
96
115
  finding.events << trigger_event
97
116
  build_hash(finding, source)
98
117
  finding.routes << context.route if context.route
99
- context.activity.findings << finding
118
+ finding.version = determine_compliance_version(finding)
100
119
  logger.trace('Finding created',
101
120
  node_id: trigger_node.id,
102
121
  source_id: source.__id__,
103
122
  rule: trigger_node.rule_id)
123
+ report_finding(finding)
104
124
  rescue StandardError => e
105
125
  logger.error('Unable to build a finding', e, rule: trigger_node.rule_id, node_id: trigger_node.id)
106
126
  end
@@ -110,8 +130,8 @@ module Contrast
110
130
  # This is our method that actually checks the taint on the object
111
131
  # our trigger_node targets.
112
132
  #
113
- # @param context [Contrast::Utils::ThreadTracker] the current request
114
- # context
133
+ # @param context [Contrast::Agent::RequestContext] the current
134
+ # request context
115
135
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode]
116
136
  # the node to direct applying this trigger event
117
137
  # @param source [Object] the source of the Trigger Event
@@ -179,8 +199,8 @@ module Contrast
179
199
  # This is our method that actually checks the taint on the object
180
200
  # our trigger_node targets for our Regexp based rules.
181
201
  #
182
- # @param context [Contrast::Utils::ThreadTracker] the current request
183
- # context
202
+ # @param context [Contrast::Agent::RequestContext] the current
203
+ # request context
184
204
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode]
185
205
  # the node to direct applying this trigger event
186
206
  # @param source [Object] the source of the Trigger Event
@@ -199,8 +219,8 @@ module Contrast
199
219
  # This is our method that actually checks the taint on the object
200
220
  # our trigger_node targets for our Dataflow based rules.
201
221
  #
202
- # @param context [Contrast::Utils::ThreadTracker] the current request
203
- # context
222
+ # @param context [Contrast::Agent::RequestContext] the current
223
+ # request context
204
224
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode]
205
225
  # the node to direct applying this trigger event
206
226
  # @param source [Object] the source of the Trigger Event
@@ -211,8 +231,8 @@ module Contrast
211
231
  def apply_dataflow_rule context, trigger_node, source, object, ret, *args
212
232
  return unless source
213
233
 
214
- if Contrast::Utils::DuckUtils.quacks_to?(source, :cs__properties)
215
- return unless source.cs__tracked?
234
+ if Contrast::Agent::Assess::Tracker.trackable?(source)
235
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
216
236
  return unless trigger_node.violated?(source)
217
237
 
218
238
  build_finding(context, trigger_node, source, object, ret, *args)
@@ -226,30 +246,27 @@ module Contrast
226
246
  apply_dataflow_rule(context, trigger_node, value, object, ret, *args)
227
247
  end
228
248
  else
229
- logger.warn('Trigger source is of unknown type. Unable to inspect.',
230
- node_id: trigger_node.id,
231
- source_id: source.__id__,
232
- source_type: source.cs__class.to_s)
249
+ logger.debug('Trigger source is untrackable. Unable to inspect.',
250
+ node_id: trigger_node.id,
251
+ source_id: source.__id__,
252
+ source_type: source.cs__class.to_s,
253
+ frozen: source.cs__frozen?)
233
254
  logger.trace(source.to_s[0..99])
234
255
  end
235
256
  end
236
257
 
237
258
  def build_from_source finding, source
238
259
  return unless source
239
- return unless Contrast::Utils::DuckUtils.quacks_to?(
240
- source,
241
- :cs__properties)
242
- return unless source.cs__properties
260
+ return unless Contrast::Agent::Assess::Tracker.trackable?(source)
243
261
 
244
- # events could technically be nil, but we would have failed
245
- # the rule check before getting here. not worth the nil check
246
- source.cs__properties.events.each do |event|
247
- finding.events << event.to_dtm_event
248
- end
262
+ properties = Contrast::Agent::Assess::Tracker.properties(source)
263
+ return unless properties
264
+
265
+ build_events finding, properties.event if properties.event
249
266
 
250
267
  # Google::Protobuf::Map doesn't support merge!, so we have to do this
251
268
  # long form
252
- source_props = source.cs__properties.properties
269
+ source_props = properties.properties
253
270
  return unless source_props
254
271
 
255
272
  source_props.each_pair do |key, value|
@@ -258,11 +275,42 @@ module Contrast
258
275
  end
259
276
  end
260
277
 
278
+ def build_events finding, event
279
+ return unless event
280
+
281
+ event.parent_events&.each do |parent_event|
282
+ build_events(finding, parent_event)
283
+ end
284
+ # events could technically be nil, but we would have failed
285
+ # the rule check before getting here. not worth the nil check
286
+ finding.events << event.to_dtm_event
287
+ end
288
+
261
289
  def build_hash finding, source
262
290
  hash_code = Contrast::Utils::HashDigest.generate_event_hash(finding, source)
263
291
  finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash_code)
264
292
  finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding)
265
293
  end
294
+
295
+ # Because our APIs are not versioned, TeamServer relies on a field,
296
+ # version, to tell it what, if any, validation it can preform on
297
+ # the findings we send it. Examine the finding and determine the
298
+ # level to which it conforms.
299
+ #
300
+ # @param finding [Contrast::Api::Dtm::Finding]
301
+ # @return [int] the version of compliance
302
+ def determine_compliance_version finding
303
+ return MINIMUM_FINDING_VERSION unless finding
304
+ # as routes are the only variable between findings, in the case
305
+ # where we couldn't determine one, any finding with a route is at
306
+ # maximum compliance
307
+ return CURRENT_FINDING_VERSION if finding.routes.any?
308
+ # any finding without events is not of a dataflow type and
309
+ # therefore at maximum compliance
310
+ return CURRENT_FINDING_VERSION unless finding.events.any?
311
+
312
+ MINIMUM_FINDING_VERSION
313
+ end
266
314
  end
267
315
  end
268
316
  end
@@ -100,16 +100,17 @@ module Contrast
100
100
  # if the source isn't tracked, there can't be a violation
101
101
  # this condition may not hold true forever, but for now it's
102
102
  # a nice optimization
103
- return false unless source.cs__tracked?
103
+ return false unless Contrast::Agent::Assess::Tracker.tracked?(source)
104
104
 
105
+ properties = Contrast::Agent::Assess::Tracker.properties(source)
105
106
  # find the ranges that violate the rule (untrusted, etc)
106
- vulnerable_ranges = find_ranges_by_all_tags(Contrast::Utils::StringUtils.ret_length(source), source.cs__properties, required_tags)
107
+ vulnerable_ranges = ranges_with_all_tags(Contrast::Utils::StringUtils.ret_length(source), properties, required_tags)
107
108
  # if there aren't any vulnerable ranges, nope out
108
109
  return false if vulnerable_ranges.empty?
109
110
 
110
111
  # find the ranges that are exempt from the rule
111
112
  # (validated, sanitized, etc)
112
- secure_ranges = find_ranges_by_any_tag(source.cs__properties, disallowed_tags)
113
+ secure_ranges = ranges_with_any_tag(properties, disallowed_tags)
113
114
  # if there are vulnerable ranges and no secure, report
114
115
  return true if secure_ranges.empty?
115
116
 
@@ -178,68 +179,62 @@ module Contrast
178
179
  # @param length [Integer] the length of the object which may have the
179
180
  # given tags -- used as the maximum index to search for all of the
180
181
  # tags.
181
- # @param cs__properties [Contrast::Agent::Assess::Properties] the
182
+ # @param properties [Contrast::Agent::Assess::Properties] the
182
183
  # properties to check for the tags
183
- # @param tags [Set<String>] the list of tags on which to match
184
+ # @param required_tags [Set<String>] the list of tags on which to match
184
185
  # @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
185
186
  # by the given conditions
186
- def find_ranges_by_all_tags length, cs__properties, tags
187
- # if there aren't any all_tags or tags, break early
188
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless cs__properties.tracked?
189
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
190
-
191
- # :zap: faster to treat all as any if there's only one tag
192
- return find_ranges_by_any_tag(cs__properties, tags) if tags.length == 1
187
+ def ranges_with_all_tags length, properties, required_tags
188
+ # if there are no tags, not required tags, or the tags don't have
189
+ # all the required tags, we can just return here.
190
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless properties.tracked?
191
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless required_tags&.any?
192
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless required_tags.all? { |tag| properties.tag_keys.include?(tag) }
193
193
 
194
194
  ranges = []
195
- # TODO: RUBY-946 clean this up, perhaps with
196
- # tags.each { |tag| applicable << cs__properties.fetch_tag(tag) }
197
- # return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless applicable.length == tags.length
198
- # ...
195
+ chunking = false
199
196
  # find all the indicies on the source that have all the given tags
200
197
  (0..length).each do |idx|
201
- tags_at = cs__properties.tags_at(idx)
202
- ranges << idx if tags.all? do |tag|
203
- found = false
204
- tags_at.each do |tag_at|
205
- found = tag_at.label == tag
206
- break if found
207
- end
208
- found
198
+ tags_at = properties.tags_at(idx).to_a
199
+ # only those that have all the required tags in the tags_at
200
+ # satisfy the requirement
201
+ satisfied = tags_at.any? && required_tags.all? { |tag| tags_at.any? { |found| found.label == tag } }
202
+ # if this range matches all the required tags and we're already
203
+ # chunking, meaning the previous range matched, do nothing
204
+ next if satisfied && chunking
205
+
206
+ # if we are satisfied and we were not chunking, this represents
207
+ # the start of the next range, so create a new entry.
208
+ if satisfied
209
+ ranges << Contrast::Agent::Assess::Tag.new('required', 0, idx)
210
+ chunking = true
211
+ # if we are chunking and not satisfied, this represents the end
212
+ # of the range, meaning the last index is what satisfied the
213
+ # range. Because the range is exclusive end, we can just use this
214
+ # index.
215
+ elsif chunking
216
+ ranges[-1]&.update_end(idx)
217
+ chunking = false
209
218
  end
210
219
  end
211
- # break early if no indicies satisfy all the tags
212
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY if ranges.empty?
213
-
214
- # chunk all the adjacent ranges
215
- chunked = ranges.chunk_while { |i, j| i + 1 == j }
216
- tag_ranges = []
217
- # and convert them into Tags
218
- chunked.each do |join|
219
- start = join[0]
220
- stop = join[-1]
221
- # add the 1 to account for end index being exclusive
222
- tag_length = stop - start + 1
223
- tag_ranges = Contrast::Utils::TagUtil.ordered_merge(tag_ranges, Tag.new(tag_length, start))
224
- end
225
- tag_ranges
220
+ ranges
226
221
  end
227
222
 
228
223
  # Find the ranges that satisfy any of the given tags.
229
224
  #
230
- # @param cs__properties [Contrast::Agent::Assess::Properties] the
225
+ # @param properties [Contrast::Agent::Assess::Properties] the
231
226
  # properties to check for the tags
232
227
  # @param tags [Set<String>] the list of tags on which to match
233
228
  # @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
234
229
  # by the given conditions
235
- def find_ranges_by_any_tag cs__properties, tags
230
+ def ranges_with_any_tag properties, tags
236
231
  # if there aren't any all_tags or tags, break early
237
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless cs__properties.tracked?
232
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless properties.tracked?
238
233
  return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
239
234
 
240
235
  ranges = []
241
236
  tags.each do |desired|
242
- found = cs__properties.fetch_tag(desired)
237
+ found = properties.fetch_tag(desired)
243
238
  next unless found
244
239
 
245
240
  # we need to dup here so that we don't change the tags if target is
@@ -38,7 +38,8 @@ module Contrast
38
38
  finish = match.begin(:path)
39
39
  finish ||= url.length
40
40
 
41
- args[0].cs__properties.any_tags_between?(start, finish)
41
+ properties = Contrast::Agent::Assess::Tracker.properties(args[0])
42
+ properties.any_tags_between?(start, finish)
42
43
  end
43
44
  end
44
45
  end
@@ -5,6 +5,7 @@ require 'base64'
5
5
  require 'set'
6
6
  require 'contrast/agent/assess/property/evented'
7
7
  require 'contrast/agent/assess/property/tagged'
8
+ require 'contrast/agent/assess/property/updated'
8
9
  require 'contrast/utils/prevent_serialization'
9
10
 
10
11
  module Contrast
@@ -20,6 +21,7 @@ module Contrast
20
21
  include Contrast::Utils::PreventSerialization
21
22
  include Contrast::Agent::Assess::Property::Evented
22
23
  include Contrast::Agent::Assess::Property::Tagged
24
+ include Contrast::Agent::Assess::Property::Updated
23
25
 
24
26
  attr_accessor :dupped_from
25
27
 
@@ -10,23 +10,11 @@ module Contrast
10
10
  module Property
11
11
  # This module serves to hold the functionality required for the
12
12
  # management of our dataflow events.
13
+ #
14
+ # @attr_reader event [Contrast::Agent::Assess::ContrastEvent] the
15
+ # latest event to track
13
16
  module Evented
14
- # The events for this object.
15
- #
16
- # @return [Array<Contrast::Agent::Assess::ContrastEvent>]
17
- def events
18
- @_events ||= []
19
- end
20
-
21
- # Add an event to these properties. It will be used to build
22
- # a trace if this object ends up in a trigger.
23
- #
24
- # @param event [Contrast::Agent::Assess::ContrastEvent] the latest
25
- # event to track
26
- def add_event event
27
- events << event
28
- self
29
- end
17
+ attr_accessor :event
30
18
 
31
19
  # Create a new event and add it to the event set.
32
20
  #
@@ -43,8 +31,7 @@ module Contrast
43
31
  # the key used to accessed if from a map or nil if a type like
44
32
  # BODY
45
33
  def build_event policy_node, tagged, object, ret, args, source_type = nil, source_name = nil
46
- event = Contrast::Agent::Assess::Events::EventFactory.build(policy_node, tagged, object, ret, args, source_type, source_name)
47
- add_event(event)
34
+ @event = Contrast::Agent::Assess::Events::EventFactory.build(policy_node, tagged, object, ret, args, source_type, source_name)
48
35
  report_sources(tagged, event)
49
36
  end
50
37
 
@@ -162,15 +162,21 @@ module Contrast
162
162
  end
163
163
 
164
164
  # We'll use this as a helper method to retrieve tags from the hash.
165
- # Because the hash auto-populates an empty array when we try to access
166
- # a tag in it, we cannot use the [] method without side effect. To get
167
- # around this, we'll use a fetch work around.
165
+ # Because the hash auto-populates an empty array when we try to
166
+ # access a tag in it, we cannot use the [] method without side
167
+ # effect. To get around this, we'll use a fetch work around.
168
+ #
169
+ # @param label [Symbol] the label to look up
170
+ # @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
171
+ # that label
168
172
  def fetch_tag label
169
173
  tags.fetch(label, nil) if tracked?
170
174
  end
171
175
 
172
176
  # Convert the tags of this object into the TraceTaintRange required
173
177
  # to be sent to the service
178
+ #
179
+ # @return [Array<Contrast::Api::Dtm::TraceTaintRange>]
174
180
  def tags_to_dtm
175
181
  Contrast::Api::Dtm::TraceTaintRange.build_for_event(tags)
176
182
  end
@@ -0,0 +1,131 @@
1
+ # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/utils/duck_utils'
5
+
6
+ module Contrast
7
+ module Agent
8
+ module Assess
9
+ module Property
10
+ # This module serves to hold the functionality required for the
11
+ # update of properties as they go through dataflow.
12
+ module Updated
13
+ # copy tags and info from source's properties to self
14
+ # @param source [Object] the object from which existing properties
15
+ # should be copied.
16
+ # @param owner [Object] the object to which these properties apply.
17
+ # @param shift [Integer] (0) how far to shift the tags during copy,
18
+ # useful for insert and append operations.
19
+ # @param skip_tags [Set<String>] (nil) the tags to not copy over,
20
+ # useful for propagation events that have 'untags'.
21
+ def copy_from source, owner, shift = 0, skip_tags = nil
22
+ return if owner.equal?(source)
23
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
24
+
25
+ original = Contrast::Agent::Assess::Tracker.properties(source)
26
+ return unless original
27
+
28
+ adjust_duplicate(original)
29
+ original.tag_keys.each do |key|
30
+ next if skip_tags&.include?(key)
31
+
32
+ existing = tags[key]
33
+ had_existing = existing.any?
34
+ value = original.fetch_tag(key)
35
+ value.each do |tag|
36
+ existing << tag.copy_modified(shift)
37
+ end
38
+ Contrast::Utils::TagUtil.size_aware_merge(owner, existing) if had_existing
39
+ end
40
+ end
41
+
42
+ # Some propagation occurred, but we're not sure what the
43
+ # exact transformation was. To be safe, we just explode
44
+ # all the tags from the source to the return.
45
+ #
46
+ # If the return already had that tag, the existing tag
47
+ # range is recycled to save us an object.
48
+ #
49
+ # @param source [Object] the object from which existing properties
50
+ # should be copied.
51
+ # @param owner [Object] the object to which these properties apply.
52
+ def splat_from source, owner
53
+ splat_length = Contrast::Utils::StringUtils.ret_length(owner)
54
+ return if splat_length.zero?
55
+
56
+ splat_from_ret(splat_length)
57
+ splat_from_source(source, splat_length)
58
+ cleanup_tags
59
+ end
60
+
61
+ private
62
+
63
+ # Because of how our tracking works now, sometimes the Source and
64
+ # Target are the same, but their IDs in our map will be different due
65
+ # to PreShift duplication. To account for this, we have to ensure that
66
+ # the Object we're copying from does not have the same Properties
67
+ # that the Object we're copying to does. If they are the same, wipe the
68
+ # Target so that the copy method can update events and ranges as
69
+ # necessary.
70
+ # DO NOT TAKE THIS OUT!
71
+ def adjust_duplicate original
72
+ reset_properties if original == self
73
+ reset_properties if original.__id__ == dupped_from
74
+ reset_properties if original.dupped_from == __id__
75
+ end
76
+
77
+ # Wipe out the instance variables on this Properties instance,
78
+ # allowing them to be rebuilt.
79
+ def reset_properties
80
+ @_tags = nil
81
+ @_events = nil
82
+ @_properties = nil
83
+ end
84
+
85
+ # Splat all the tags from the source to this set of Properties
86
+ #
87
+ # @param source [Object] the object from which tags will be copied
88
+ # and splatted.
89
+ # @param splat_length [Integer] the length to which to to set all
90
+ # tags.
91
+ def splat_from_source source, splat_length
92
+ properties = Contrast::Agent::Assess::Tracker.properties(source)
93
+ return unless properties
94
+
95
+ properties.tag_keys.each do |key|
96
+ existing = fetch_tag(key)
97
+ # if the tag already exists, drop all but the first range
98
+ # then change that range to cover the entire return
99
+ if existing
100
+ existing.drop(existing.length - 1)
101
+ range = existing[0]
102
+ range.repurpose(0, splat_length)
103
+ else
104
+ add_tag(key, 0...splat_length)
105
+ end
106
+ end
107
+ end
108
+
109
+ # Splat all the tags existing on this set of Properties
110
+ #
111
+ # @param splat_length [Integer] the length to which to to set all
112
+ # tags.
113
+ def splat_from_ret splat_length
114
+ return unless tracked?
115
+
116
+ tag_keys.each do |key|
117
+ next unless key
118
+
119
+ existing = fetch_tag(key)
120
+ next unless existing
121
+
122
+ existing.each do |range|
123
+ range.repurpose(0, splat_length)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end