contrast-agent 3.14.0 → 3.15.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 (86) 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/assess.rb +1 -0
  10. data/lib/contrast/agent/assess/contrast_event.rb +4 -12
  11. data/lib/contrast/agent/assess/finalizers/freeze.rb +3 -1
  12. data/lib/contrast/agent/assess/finalizers/hash.rb +45 -1
  13. data/lib/contrast/agent/assess/policy/patcher.rb +1 -1
  14. data/lib/contrast/agent/assess/policy/policy.rb +0 -2
  15. data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -1
  16. data/lib/contrast/agent/assess/policy/preshift.rb +7 -11
  17. data/lib/contrast/agent/assess/policy/propagation_method.rb +50 -33
  18. data/lib/contrast/agent/assess/policy/propagator/append.rb +8 -5
  19. data/lib/contrast/agent/assess/policy/propagator/base.rb +1 -1
  20. data/lib/contrast/agent/assess/policy/propagator/center.rb +9 -5
  21. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +5 -3
  22. data/lib/contrast/agent/assess/policy/propagator/insert.rb +6 -3
  23. data/lib/contrast/agent/assess/policy/propagator/keep.rb +4 -1
  24. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +6 -6
  25. data/lib/contrast/agent/assess/policy/propagator/next.rb +7 -5
  26. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +8 -5
  27. data/lib/contrast/agent/assess/policy/propagator/remove.rb +8 -4
  28. data/lib/contrast/agent/assess/policy/propagator/replace.rb +5 -2
  29. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +7 -5
  30. data/lib/contrast/agent/assess/policy/propagator/select.rb +15 -7
  31. data/lib/contrast/agent/assess/policy/propagator/splat.rb +14 -8
  32. data/lib/contrast/agent/assess/policy/propagator/split.rb +14 -8
  33. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +30 -21
  34. data/lib/contrast/agent/assess/policy/propagator/trim.rb +11 -5
  35. data/lib/contrast/agent/assess/policy/source_method.rb +85 -58
  36. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +16 -11
  37. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  38. data/lib/contrast/agent/assess/policy/trigger_method.rb +38 -15
  39. data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -13
  40. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +2 -1
  41. data/lib/contrast/agent/assess/properties.rb +2 -0
  42. data/lib/contrast/agent/assess/property/updated.rb +136 -0
  43. data/lib/contrast/agent/assess/tracker.rb +66 -0
  44. data/lib/contrast/agent/class_reopener.rb +7 -5
  45. data/lib/contrast/agent/middleware.rb +0 -1
  46. data/lib/contrast/agent/patching/policy/patcher.rb +13 -22
  47. data/lib/contrast/agent/patching/policy/policy.rb +1 -4
  48. data/lib/contrast/agent/response.rb +17 -6
  49. data/lib/contrast/agent/rewriter.rb +1 -3
  50. data/lib/contrast/agent/version.rb +1 -1
  51. data/lib/contrast/api/communication/messaging_queue.rb +1 -4
  52. data/lib/contrast/api/decorators/application_update.rb +2 -4
  53. data/lib/contrast/api/decorators/trace_event.rb +5 -5
  54. data/lib/contrast/components/app_context.rb +11 -9
  55. data/lib/contrast/components/config.rb +3 -13
  56. data/lib/contrast/components/contrast_service.rb +2 -2
  57. data/lib/contrast/config/application_configuration.rb +5 -2
  58. data/lib/contrast/config/service_configuration.rb +8 -2
  59. data/lib/contrast/configuration.rb +88 -47
  60. data/lib/contrast/extension/assess.rb +0 -2
  61. data/lib/contrast/extension/assess/array.rb +8 -5
  62. data/lib/contrast/extension/assess/erb.rb +6 -3
  63. data/lib/contrast/extension/assess/fiber.rb +9 -9
  64. data/lib/contrast/extension/assess/hash.rb +2 -3
  65. data/lib/contrast/extension/assess/kernel.rb +12 -5
  66. data/lib/contrast/extension/assess/marshal.rb +3 -2
  67. data/lib/contrast/extension/assess/regexp.rb +5 -4
  68. data/lib/contrast/extension/assess/string.rb +8 -10
  69. data/lib/contrast/framework/rack/patch/session_cookie.rb +12 -18
  70. data/lib/contrast/framework/rails/patch/assess_configuration.rb +4 -10
  71. data/lib/contrast/framework/rails/support.rb +2 -0
  72. data/lib/contrast/logger/application.rb +11 -3
  73. data/lib/contrast/utils/assess/tracking_util.rb +48 -3
  74. data/lib/contrast/utils/duck_utils.rb +0 -10
  75. data/lib/contrast/utils/env_configuration_item.rb +2 -1
  76. data/lib/contrast/utils/invalid_configuration_util.rb +21 -19
  77. data/lib/contrast/utils/string_utils.rb +10 -5
  78. data/resources/assess/policy.json +0 -10
  79. data/ruby-agent.gemspec +16 -15
  80. data/service_executables/VERSION +1 -1
  81. data/service_executables/linux/contrast-service +0 -0
  82. data/service_executables/mac/contrast-service +0 -0
  83. metadata +42 -21
  84. data/lib/contrast/agent/assess/finalizers/finalize.rb +0 -21
  85. data/lib/contrast/extension/assess/assess_extension.rb +0 -145
  86. data/lib/contrast/utils/freeze_util.rb +0 -32
@@ -46,8 +46,7 @@ module Contrast
46
46
 
47
47
  begin
48
48
  source = preshift.object
49
- self_tracked = Contrast::Utils::DuckUtils.quacks_to?(source, :cs__tracked?) &&
50
- source.cs__tracked?
49
+ self_tracked = Contrast::Agent::Assess::Tracker.tracked?(source)
51
50
  args = preshift.args[1]
52
51
  incoming_tracked = args && determine_tracked(args)
53
52
  return ret unless self_tracked || incoming_tracked
@@ -75,10 +74,10 @@ module Contrast
75
74
  # if it's a string, just ask if it's tracked
76
75
  case args
77
76
  when String
78
- Contrast::Utils::DuckUtils.quacks_to?(args, :cs__tracked?) && args.cs__tracked?
77
+ Contrast::Agent::Assess::Tracker.tracked?(args)
79
78
  # if it's a hash, ask if it has a tracked string
80
79
  when Hash
81
- args.values.any? { |value| value.is_a?(String) && Contrast::Utils::DuckUtils.quacks_to?(value, :cs__tracked?) && value.cs__tracked? }
80
+ args.values.any? { |value| value.is_a?(String) && Contrast::Agent::Assess::Tracker.tracked?(value) }
82
81
  # this should never happen
83
82
  else
84
83
  false
@@ -86,6 +85,9 @@ module Contrast
86
85
  end
87
86
 
88
87
  def string_sub self_tracked, preshift, ret, incoming, incoming_tracked, global
88
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
89
+ return unless properties
90
+
89
91
  pattern = preshift.args[0]
90
92
  source = preshift.object
91
93
 
@@ -93,13 +95,13 @@ module Contrast
93
95
  # copied from regexp / captures. Trading accuracy for
94
96
  # performance
95
97
  if incoming.match?(CAPTURE_GROUP_REGEXP) || incoming.match?(CAPTURE_NAME_REGEXP)
96
- source.cs__splat_tags(ret) if self_tracked
98
+ properties.splat_from(source, ret) if self_tracked
97
99
  return
98
100
  end
99
101
 
100
102
  # if it's just a straight insert, that we can do
101
103
  # Copy the tags from us to the return
102
- ret.cs__copy_from(source)
104
+ properties.copy_from(source, ret)
103
105
  # Figure out where inserts occurred
104
106
  last_idx = 0
105
107
  ranges = []
@@ -114,48 +116,55 @@ module Contrast
114
116
  ranges << (start_index...end_index)
115
117
  break unless global
116
118
  end
117
- ret.cs__properties.delete_tags_at_ranges(ranges)
118
- ret.cs__properties.shift_tags(ranges)
119
+ properties.delete_tags_at_ranges(ranges)
120
+ properties.shift_tags(ranges)
119
121
  return unless incoming_tracked
120
122
 
121
- tags = incoming.cs__properties.tag_keys
123
+ incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming)
124
+ tags = incoming_properties.tag_keys
122
125
  ranges.each do |range|
123
126
  tags.each do |tag|
124
- ret.cs__properties.add_tag(tag, range)
127
+ properties.add_tag(tag, range)
125
128
  end
126
129
  end
127
130
  end
128
131
 
129
132
  def block_sub self_tracked, source, ret
130
- source.cs__splat_tags(ret) if self_tracked
133
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
134
+ properties&.splat_from(source, ret) if self_tracked
131
135
  end
132
136
 
133
137
  def hash_sub self_tracked, source, ret
134
- source.cs__splat_tags(ret) if self_tracked
138
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
139
+ properties&.splat_from(source, ret) if self_tracked
135
140
  end
136
141
 
137
142
  def pattern_gsub preshift, ret
138
- return unless Contrast::Utils::DuckUtils.trackable?(ret)
143
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
144
+ return unless properties
139
145
 
140
146
  source = preshift.object
141
- source.cs__properties.tag_keys.each do |key|
142
- ret.cs__properties.add_tag(key, 0...1)
147
+ source_properties = Contrast::Agent::Assess::Tracker.properties(source)
148
+ return unless source_properties
149
+
150
+ source_properties.tag_keys.each do |key|
151
+ properties.add_tag(key, 0...1)
143
152
  end
144
153
  end
145
154
 
146
155
  def string_build_event patcher, preshift, ret
147
- return unless Contrast::Utils::DuckUtils.quacks_to?(ret, :cs__tracked?) && ret.cs__tracked?
156
+ return unless Contrast::Agent::Assess::Tracker.tracked?(ret)
148
157
 
158
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
149
159
  args = preshift.args
150
160
  if args.length > 1
151
161
  arg = args[1]
152
- if Contrast::Utils::DuckUtils.quacks_to?(arg, :cs__properties)
153
- arg.cs__properties.events.each do |event|
154
- ret.cs__properties.events << event
155
- end
162
+ arg_properties = Contrast::Agent::Assess::Tracker.properties(arg)
163
+ arg_properties&.events&.each do |event|
164
+ properties.events << event
156
165
  end
157
166
  end
158
- ret.cs__properties.build_event(
167
+ properties.build_event(
159
168
  patcher,
160
169
  ret,
161
170
  preshift.object,
@@ -16,9 +16,12 @@ module Contrast
16
16
  def tr_tagger patcher, preshift, ret, _block
17
17
  return ret unless ret && !ret.empty?
18
18
 
19
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
20
+ return unless properties
21
+
19
22
  source = preshift.object
20
23
  args = preshift.args
21
- ret.cs__copy_from(source)
24
+ properties.copy_from(source, ret)
22
25
  replace_string = args[1]
23
26
  source_chars = source.chars
24
27
  # if the replace string is empty, then there's a bunch of deletes. this
@@ -41,10 +44,10 @@ module Contrast
41
44
  end
42
45
  # account for the last char being different
43
46
  remove_ranges << (start...source_chars.length) if start
44
- ret.cs__properties.delete_tags_at_ranges(remove_ranges, false)
47
+ properties.delete_tags_at_ranges(remove_ranges, false)
45
48
  end
46
49
 
47
- ret.cs__properties.build_event(
50
+ properties.build_event(
48
51
  patcher,
49
52
  ret,
50
53
  source,
@@ -57,10 +60,13 @@ module Contrast
57
60
  def tr_s_tagger patcher, preshift, ret, _block
58
61
  return unless ret && !ret.empty?
59
62
 
63
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
64
+ return unless properties
65
+
60
66
  source = preshift.object
61
67
  args = preshift.args
62
- source.cs__splat_tags(ret)
63
- ret.cs__properties.build_event(
68
+ properties.splat_from(source, ret)
69
+ properties.build_event(
64
70
  patcher,
65
71
  ret,
66
72
  source,
@@ -17,7 +17,7 @@ module Contrast
17
17
  # dataflows used in Assess vulnerability detection.
18
18
  module SourceMethod
19
19
  include Contrast::Components::Interface
20
- access_component :logging, :analysis
20
+ access_component :analysis, :logging
21
21
 
22
22
  PARAMETER_TYPE = 'PARAMETER'
23
23
  PARAMETER_KEY_TYPE = 'PARAMETER_KEY'
@@ -44,38 +44,26 @@ module Contrast
44
44
  current_context = Contrast::Agent::REQUEST_TRACKER.current
45
45
  return unless current_context&.analyze_request? && ASSESS.enabled?
46
46
 
47
- replaced_return = nil
48
47
  source_node = method_policy.source_node
49
-
50
48
  target = determine_target(source_node, object, ret, args)
49
+ restore_frozen_state = false
50
+ if target.cs__frozen? && !Contrast::Agent::Assess::Tracker.trackable?(target)
51
+ return unless ASSESS.track_frozen_sources?
52
+ return unless source_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
51
53
 
52
- # We don't propagate to frozen things that haven't been tracked
53
- # before. By definition, something that is a source has not
54
- # previously been tracked; therefore, we can break out early.
55
- if target.cs__frozen?
56
- # That being said, we don't have enough context to know if we
57
- # can make this assumption and still function, so we'll allow for
58
- # source tracking of frozen things by a common config setting.
59
- #
60
- # Rails' StrongParameters make a case for this to be default
61
- # behavior
62
- return replaced_return unless ASSESS.track_frozen_sources?
63
-
64
- # If we're tracking the frozen target, we need to unfreeze
65
- # (dup) it to track and then freeze that result. For
66
- # simplicities sake, we ONLY do this if the return is the
67
- # target (I don't want to have to deal with unfreezing self)
68
- return replaced_return unless source_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
54
+ dup = safe_dup(ret)
55
+ return unless dup
69
56
 
70
57
  restore_frozen_state = true
71
- ret = Contrast::Utils::FreezeUtil.unfreeze_dup(ret)
58
+ ret = dup
72
59
  target = ret
60
+ Contrast::Agent::Assess::Tracker.pre_freeze(ret)
61
+ ret.cs__freeze
62
+ # double check that we were able to finalize the replaced return
63
+ return unless Contrast::Agent::Assess::Tracker.trackable?(target)
73
64
  end
74
-
75
65
  apply_source(current_context, source_node, target, object, ret, source_node.type, nil, *args)
76
-
77
- ret.cs__freeze if restore_frozen_state
78
- ret
66
+ restore_frozen_state ? ret : nil
79
67
  end
80
68
 
81
69
  private
@@ -103,34 +91,10 @@ module Contrast
103
91
  source_name ||= determine_source_name(source_node, object, ret, *args)
104
92
  # We know we only work on certain things.
105
93
  # Skip if this isn't one of them
106
- if Contrast::Utils::DuckUtils.quacks_to?(target, :cs__properties)
94
+ if Contrast::Agent::Assess::Tracker.trackable?(target)
107
95
  apply_tags(source_node, target, object, ret, source_type, source_name, *args)
108
- # While we don't taint hashes themselves, we may taint the things
109
- # they hold. Let's pass their keys and values back to ourselves and
110
- # try again
111
96
  elsif Contrast::Utils::DuckUtils.iterable_hash?(target)
112
- to_replace = []
113
- target.each_pair do |key, value|
114
- # We only do this for Strings b/c of the way Hash lookup works.
115
- # To replace another object would break hash lookup and,
116
- # therefore, the application
117
- if can_track_hash_key?(key, target)
118
- key = Contrast::Utils::FreezeUtil.unfreeze_dup(key)
119
- to_replace << key
120
- end
121
- apply_source(context, source_node, key, object, ret, key_type(source_type), key, *args)
122
- apply_source(context, source_node, value, object, ret, source_type, key, *args)
123
- end
124
-
125
- # Hash is designed to keep one instance of the string key in it.
126
- # We need to remove the existing one and replace it with our new
127
- # tracked one.
128
- to_replace.each do |key|
129
- key.cs__freeze
130
- value = target[key]
131
- target.delete(key)
132
- target[key] = value
133
- end
97
+ apply_hash_tags(context, source_node, target, object, ret, source_type, *args)
134
98
  # While we don't taint arrays themselves, we may taint the things
135
99
  # they hold. Let's pass their keys and values back to ourselves and
136
100
  # try again
@@ -141,17 +105,81 @@ module Contrast
141
105
  logger.warn('Unable to apply source', e, node_id: source_node.id)
142
106
  end
143
107
 
144
- def can_track_hash_key? key, target
108
+ # While we don't taint hashes themselves, we may taint the things
109
+ # they hold. Let's pass their keys and values back to ourselves and
110
+ # try again
111
+ #
112
+ # @param context [Contrast::Utils::ThreadTracker] the current request
113
+ # context
114
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode]
115
+ # the node to direct applying this source event
116
+ # @param target [Object] the target of the Source Event
117
+ # @param object [Object] the Object on which the method was invoked
118
+ # @param ret [Object] the Return of the invoked method
119
+ # @param source_type [String] the type of this source, from the
120
+ # source_node, or a KEY_TYPE if invoked for a map
121
+ # @param args [Array<Object>] the Arguments with which the method
122
+ # was invoked
123
+ def apply_hash_tags context, source_node, target, object, ret, source_type, *args
124
+ to_replace = []
125
+ target.each_pair do |key, value|
126
+ # We only do this for Strings b/c of the way Hash lookup works.
127
+ # To replace another object would break hash lookup and,
128
+ # therefore, the application
129
+ if replace_hash_key?(key, target)
130
+ key = key.dup
131
+ to_replace << key
132
+ end
133
+ apply_source(context, source_node, key, object, ret, key_type(source_type), key, *args)
134
+ apply_source(context, source_node, value, object, ret, source_type, key, *args)
135
+ end
136
+ handle_hash_key(target, to_replace)
137
+ end
138
+
139
+ # Given an unfrozen hash, if the key is a String, we should replace
140
+ # it with one that we can finalize, allowing us to track that key.
141
+ # This method handles checking if that replace can and should
142
+ # occur.
143
+ #
144
+ # @param key [Object] the key in the hash that may need replacing.
145
+ # @param hash [Hash] the hash to which the key belongs.
146
+ # @return [Boolean] whether replace the key in the hash or not.
147
+ def replace_hash_key? key, hash
145
148
  ASSESS.track_frozen_sources? &&
149
+ !hash.cs__frozen? &&
146
150
  key.is_a?(String) &&
147
- Contrast::Utils::DuckUtils.quacks_to?(target, :delete)
151
+ !Contrast::Agent::Assess::Tracker.trackable?(key)
152
+ end
153
+
154
+ # Safely duplicate the target, or return nil
155
+ #
156
+ # @param target [Object] the thing to check for duplication
157
+ def safe_dup target
158
+ target.dup
159
+ rescue StandardError => _e
160
+ nil
161
+ end
162
+
163
+ # Hash is designed to keep one instance of the string key in it.
164
+ # We need to remove the existing one and replace it with our new
165
+ # tracked one.
166
+ def handle_hash_key target, to_replace
167
+ to_replace.each do |key|
168
+ Contrast::Agent::Assess::Tracker.pre_freeze(key)
169
+ key.cs__freeze
170
+ value = target.delete(key)
171
+ target[key] = value
172
+ end
148
173
  end
149
174
 
150
175
  def apply_tags source_node, target, object, ret, source_type, source_name, *args
176
+ # don't apply tags if we can't track the thing
177
+ return unless Contrast::Agent::Assess::Tracker.trackable?(target)
151
178
  # don't apply second source -- probably needs tuning later if we
152
179
  # use more than 'UNTRUSTED' in our sources
153
- return if target.cs__tracked? || target.cs__frozen?
180
+ return if Contrast::Agent::Assess::Tracker.tracked?(target)
154
181
 
182
+ properties = Contrast::Agent::Assess::Tracker.properties(target)
155
183
  # otherwise for each tag this source_node applies, create a tag range
156
184
  # on the target object
157
185
  # I realize this looping is counter-intuitive from the above
@@ -160,16 +188,15 @@ module Contrast
160
188
  next unless Contrast::Agent::Assess::Policy::SourceValidation.valid?(tag, source_type, source_name)
161
189
 
162
190
  length = Contrast::Utils::StringUtils.ret_length(target)
163
- target.cs__properties.add_tag(tag, 0...length)
164
- target.cs__properties.add_properties(source_node.properties)
191
+ properties.add_tag(tag, 0...length)
192
+ properties.add_properties(source_node.properties)
165
193
  logger.trace('Source detected',
166
194
  node_id: source_node.id,
167
195
  target_id: target.__id__,
168
196
  tag: tag)
169
197
  end
170
-
171
198
  # make a representation of this method that TeamServer can render
172
- target.cs__properties.build_event(source_node, target, object, ret, args, source_type, source_name)
199
+ properties.build_event(source_node, target, object, ret, args, source_type, source_name)
173
200
  end
174
201
 
175
202
  # Find the name of the source
@@ -25,24 +25,27 @@ module Contrast
25
25
  TEMPLATE_PROPAGATION_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(NODE_HASH)
26
26
 
27
27
  def xss_tilt_trigger context, trigger_node, _source, object, ret, *args
28
- scope = args[0]
28
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
29
+ return unless properties
29
30
 
31
+ scope = args[0]
30
32
  erb_template_prerender = object.instance_variable_get(:@data)
31
33
  interpolated_inputs = []
32
34
  handle_binding_variables(scope, erb_template_prerender, ret, interpolated_inputs)
33
-
34
35
  handle_local_variables(args, erb_template_prerender, ret, interpolated_inputs)
35
-
36
36
  unless interpolated_inputs.empty?
37
37
  interpolated_inputs.each do |input|
38
- input.cs__properties.events.each do |event|
39
- ret.cs__properties.events << event
38
+ input_properties = Contrast::Agent::Assess::Tracker.properties(input)
39
+ next unless input_properties
40
+
41
+ input_properties.events.each do |event|
42
+ properties.events << event
40
43
  end
41
44
  end
42
- ret.cs__properties.build_event(TEMPLATE_PROPAGATION_NODE, ret, erb_template_prerender, ret, interpolated_inputs)
45
+ properties.build_event(TEMPLATE_PROPAGATION_NODE, ret, erb_template_prerender, ret, interpolated_inputs)
43
46
  end
44
47
 
45
- if ret.cs__tracked?
48
+ if Contrast::Agent::Assess::Tracker.tracked?(ret)
46
49
  Contrast::Agent::Assess::Policy::TriggerMethod.build_finding(context, trigger_node, ret, erb_template_prerender, ret, interpolated_inputs)
47
50
  end
48
51
 
@@ -52,32 +55,34 @@ module Contrast
52
55
  private
53
56
 
54
57
  def handle_binding_variables scope, erb_template_prerender, ret, interpolated_inputs
58
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
55
59
  binding_variables = scope.instance_variables
56
60
 
57
61
  binding_variables.each do |bound_variable_sym|
58
62
  bound_variable_value = scope.instance_variable_get(bound_variable_sym)
59
63
 
60
- next unless bound_variable_value.cs__respond_to?(:cs__tracked?) && bound_variable_value.cs__tracked?
64
+ next unless Contrast::Agent::Assess::Tracker.tracked?(bound_variable_value)
61
65
  next unless erb_template_prerender.include?(bound_variable_sym.to_s)
62
66
 
63
67
  start_index = ret.index(bound_variable_value)
64
68
  next if start_index.nil?
65
69
 
66
- ret.cs__copy_from(bound_variable_value, start_index)
70
+ properties.copy_from(bound_variable_value, ret, start_index)
67
71
  interpolated_inputs << bound_variable_sym
68
72
  end
69
73
  end
70
74
 
71
75
  def handle_local_variables args, erb_template_prerender, ret, interpolated_inputs
76
+ properties = Contrast::Agent::Assess::Tracker.properties(ret)
72
77
  locals = args[1]
73
78
  locals.each do |local_name, local_value|
74
- next unless local_value.cs__respond_to?(:cs__tracked?) && local_value.cs__tracked?
79
+ next unless Contrast::Agent::Assess::Tracker.tracked?(local_value)
75
80
  next unless erb_template_prerender.include?(local_name.to_s)
76
81
 
77
82
  start_index = ret.index(local_value)
78
83
  next if start_index.nil?
79
84
 
80
- ret.cs__copy_from(local_value, start_index)
85
+ properties.copy_from(local_value, ret, start_index)
81
86
  interpolated_inputs << local_name
82
87
  end
83
88
  end
@@ -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,8 +20,11 @@ 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
27
30
  # This is called from within our woven proc. It will be called as if it
@@ -89,13 +92,12 @@ module Contrast
89
92
 
90
93
  finding = Contrast::Api::Dtm::Finding.new
91
94
  finding.rule_id = Contrast::Utils::StringUtils.protobuf_safe_string(trigger_node.rule_id)
92
- finding.version = CURRENT_FINDING_VERSION
93
-
94
95
  build_from_source(finding, source)
95
96
  trigger_event = Contrast::Agent::Assess::Events::EventFactory.build(trigger_node, source, object, ret, args).to_dtm_event
96
97
  finding.events << trigger_event
97
98
  build_hash(finding, source)
98
99
  finding.routes << context.route if context.route
100
+ finding.version = determine_compliance_version(finding)
99
101
  context.activity.findings << finding
100
102
  logger.trace('Finding created',
101
103
  node_id: trigger_node.id,
@@ -211,8 +213,8 @@ module Contrast
211
213
  def apply_dataflow_rule context, trigger_node, source, object, ret, *args
212
214
  return unless source
213
215
 
214
- if Contrast::Utils::DuckUtils.quacks_to?(source, :cs__properties)
215
- return unless source.cs__tracked?
216
+ if Contrast::Agent::Assess::Tracker.trackable?(source)
217
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
216
218
  return unless trigger_node.violated?(source)
217
219
 
218
220
  build_finding(context, trigger_node, source, object, ret, *args)
@@ -226,30 +228,31 @@ module Contrast
226
228
  apply_dataflow_rule(context, trigger_node, value, object, ret, *args)
227
229
  end
228
230
  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)
231
+ logger.debug('Trigger source is untrackable. Unable to inspect.',
232
+ node_id: trigger_node.id,
233
+ source_id: source.__id__,
234
+ source_type: source.cs__class.to_s,
235
+ frozen: source.cs__frozen?)
233
236
  logger.trace(source.to_s[0..99])
234
237
  end
235
238
  end
236
239
 
237
240
  def build_from_source finding, source
238
241
  return unless source
239
- return unless Contrast::Utils::DuckUtils.quacks_to?(
240
- source,
241
- :cs__properties)
242
- return unless source.cs__properties
242
+ return unless Contrast::Agent::Assess::Tracker.trackable?(source)
243
+
244
+ properties = Contrast::Agent::Assess::Tracker.properties(source)
245
+ return unless properties
243
246
 
244
247
  # events could technically be nil, but we would have failed
245
248
  # the rule check before getting here. not worth the nil check
246
- source.cs__properties.events.each do |event|
249
+ properties.events.each do |event|
247
250
  finding.events << event.to_dtm_event
248
251
  end
249
252
 
250
253
  # Google::Protobuf::Map doesn't support merge!, so we have to do this
251
254
  # long form
252
- source_props = source.cs__properties.properties
255
+ source_props = properties.properties
253
256
  return unless source_props
254
257
 
255
258
  source_props.each_pair do |key, value|
@@ -263,6 +266,26 @@ module Contrast
263
266
  finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash_code)
264
267
  finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding)
265
268
  end
269
+
270
+ # Because our APIs are not versioned, TeamServer relies on a field,
271
+ # version, to tell it what, if any, validation it can preform on
272
+ # the findings we send it. Examine the finding and determine the
273
+ # level to which it conforms.
274
+ #
275
+ # @param finding [Contrast::Api::Dtm::Finding]
276
+ # @return [int] the version of compliance
277
+ def determine_compliance_version finding
278
+ return MINIMUM_FINDING_VERSION unless finding
279
+ # as routes are the only variable between findings, in the case
280
+ # where we couldn't determine one, any finding with a route is at
281
+ # maximum compliance
282
+ return CURRENT_FINDING_VERSION if finding.routes.any?
283
+ # any finding without events is not of a dataflow type and
284
+ # therefore at maximum compliance
285
+ return CURRENT_FINDING_VERSION unless finding.events.any?
286
+
287
+ MINIMUM_FINDING_VERSION
288
+ end
266
289
  end
267
290
  end
268
291
  end