contrast-agent 4.12.0 → 4.13.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cs__assess_module/cs__assess_module.c +48 -0
  3. data/ext/cs__assess_module/cs__assess_module.h +7 -0
  4. data/ext/cs__common/cs__common.c +5 -0
  5. data/ext/cs__common/cs__common.h +8 -0
  6. data/ext/cs__contrast_patch/cs__contrast_patch.c +16 -1
  7. data/ext/cs__os_information/cs__os_information.c +31 -0
  8. data/ext/cs__os_information/cs__os_information.h +7 -0
  9. data/ext/cs__os_information/extconf.rb +5 -0
  10. data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
  11. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  12. data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
  13. data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -106
  14. data/lib/contrast/agent/assess/property/tagged.rb +2 -128
  15. data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
  16. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +1 -0
  17. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  18. data/lib/contrast/agent/middleware.rb +22 -0
  19. data/lib/contrast/agent/patching/policy/patch.rb +28 -235
  20. data/lib/contrast/agent/patching/policy/patcher.rb +2 -41
  21. data/lib/contrast/agent/request_handler.rb +7 -3
  22. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
  23. data/lib/contrast/agent/static_analysis.rb +4 -2
  24. data/lib/contrast/agent/telemetry.rb +129 -0
  25. data/lib/contrast/agent/telemetry_event.rb +34 -0
  26. data/lib/contrast/agent/thread_watcher.rb +43 -14
  27. data/lib/contrast/agent/version.rb +1 -1
  28. data/lib/contrast/agent.rb +6 -0
  29. data/lib/contrast/components/api.rb +34 -0
  30. data/lib/contrast/components/app_context.rb +24 -0
  31. data/lib/contrast/components/config.rb +90 -11
  32. data/lib/contrast/components/contrast_service.rb +6 -0
  33. data/lib/contrast/config/api_configuration.rb +22 -0
  34. data/lib/contrast/config/env_variables.rb +25 -0
  35. data/lib/contrast/config/root_configuration.rb +1 -0
  36. data/lib/contrast/config/service_configuration.rb +2 -1
  37. data/lib/contrast/config.rb +1 -0
  38. data/lib/contrast/configuration.rb +3 -0
  39. data/lib/contrast/framework/manager.rb +14 -12
  40. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  41. data/lib/contrast/framework/rails/patch/support.rb +31 -29
  42. data/lib/contrast/logger/application.rb +4 -0
  43. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  44. data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
  45. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  46. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  47. data/lib/contrast/utils/exclude_key.rb +20 -0
  48. data/lib/contrast/utils/metrics_hash.rb +59 -0
  49. data/lib/contrast/utils/os.rb +23 -0
  50. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  51. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  52. data/lib/contrast/utils/requests_client.rb +150 -0
  53. data/lib/contrast/utils/telemetry.rb +78 -0
  54. data/lib/contrast/utils/telemetry_identifier.rb +137 -0
  55. data/lib/contrast.rb +18 -0
  56. data/ruby-agent.gemspec +2 -1
  57. data/service_executables/VERSION +1 -1
  58. data/service_executables/linux/contrast-service +0 -0
  59. data/service_executables/mac/contrast-service +0 -0
  60. metadata +32 -10
@@ -7,6 +7,7 @@ require 'fileutils'
7
7
  require 'contrast/config'
8
8
  require 'contrast/utils/object_share'
9
9
  require 'contrast/components/scope'
10
+ require 'contrast/utils/exclude_key'
10
11
 
11
12
  module Contrast
12
13
  # This is how we read in the local settings for the Agent, both ENV/ CMD line
@@ -218,6 +219,8 @@ module Contrast
218
219
  case convert
219
220
  when Contrast::Config::BaseConfiguration
220
221
  convert.cs__class::KEYS.each_key do |key|
222
+ next if Contrast::Utils::ExcludeKey.excludable? key.to_s
223
+
221
224
  hash[key] = convert_to_hash(convert.send(key), {})
222
225
  end
223
226
  hash
@@ -16,9 +16,9 @@ module Contrast
16
16
  class Manager
17
17
  include Contrast::Components::Logger::InstanceMethods
18
18
 
19
- # Order here does matter as the first framework listed will be the first one we pull information from
20
- # Rack will be a special case that may involve updating some logic to handle only applying Rack if Rails/Sinatra
21
- # do not exist
19
+ # Order here does matter as the first framework listed will be the first one we pull information from Rack will
20
+ # be a special case that may involve updating some logic to handle only applying Rack if Rails/Sinatra do not
21
+ # exist
22
22
  SUPPORTED_FRAMEWORKS = [
23
23
  Contrast::Framework::Rails::Support, Contrast::Framework::Sinatra::Support,
24
24
  Contrast::Framework::Grape::Support, Contrast::Framework::Rack::Support
@@ -34,9 +34,8 @@ module Contrast
34
34
  @_frameworks.compact!
35
35
  end
36
36
 
37
- # Patches that have to be applied as early as possible to catch calls
38
- # that happen prior to the first Request, typically those around
39
- # configuration.
37
+ # Patches that have to be applied as early as possible to catch calls that happen prior to the first Request,
38
+ # typically those around configuration.
40
39
  def before_load_patches!
41
40
  @_before_load_patches ||= begin
42
41
  SUPPORTED_FRAMEWORKS.each(&:before_load_patches!)
@@ -81,8 +80,8 @@ module Contrast
81
80
 
82
81
  # Build a request from the provided env, based on the framework(s) we're currently supporting.
83
82
  #
84
- # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values
85
- # of this particular Request
83
+ # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
84
+ # this particular Request
86
85
  # @return [::Rack::Request] either a rack request or subclass thereof.
87
86
  def retrieve_request env
88
87
  # If we're mounted on Rails, use Rails.
@@ -99,8 +98,8 @@ module Contrast
99
98
  logger.warn('Unable to retrieve_request', e)
100
99
  end
101
100
 
102
- # @param env [Hash] the various variables stored by this and other Middlewares to know the state
103
- # and values of this particular Request
101
+ # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
102
+ # this particular Request
104
103
  # @return [Boolean] true if at least one framework is streaming the response; false if none are streaming
105
104
  def streaming? env
106
105
  result = false
@@ -111,8 +110,8 @@ module Contrast
111
110
  result
112
111
  end
113
112
 
114
- # Iterate through current frameworks and return the current request's route. This will be the first
115
- # non-nil result.
113
+ # Iterate through current frameworks and return the current request's route. This will be the first non-nil
114
+ # result.
116
115
  #
117
116
  # @param request [Contrast::Agent::Request] the current request.
118
117
  # @return [Contrast::Api::Dtm::RouteCoverage] the current route as a Dtm.
@@ -125,6 +124,9 @@ module Contrast
125
124
  # have support enabled, we'll enable it now. We'll also need to catch up on any other startup actions that we've
126
125
  # missed. Most likely, this is only necessary for those applications which have applications mounted on them.
127
126
  #
127
+ # TODO: RUBY-1354
128
+ # TODO: RUBY-1356
129
+ #
128
130
  # @param mod [Module] the module or class that was just loaded
129
131
  def register_late_framework mod
130
132
  return unless mod
@@ -5,10 +5,14 @@ module Contrast
5
5
  module Framework
6
6
  module Rails
7
7
  module Patch
8
- # This class acts as our patch into the ActionController::Live::Buffer
9
- # class, allowing us to track the close event on streamed responses.
8
+ # This class acts as our patch into the ActionController::Live::Buffer class, allowing us to track the close
9
+ # event on streamed responses.
10
10
  module ActionControllerLiveBuffer
11
11
  class << self
12
+ # TODO: RUBY-1353
13
+ # TODO: RUBY-1355
14
+ # TODO: RUBY-1357
15
+ # TODO: RUBY-1357
12
16
  def send_messages
13
17
  return unless (context = Contrast::Agent::REQUEST_TRACKER.current)
14
18
 
@@ -20,10 +24,9 @@ module Contrast
20
24
  def instrument
21
25
  @_instrument ||= begin
22
26
  ::ActionController::Live::Buffer.class_eval do
23
- # normally pre->in->post filters are applied however, in a streamed response
24
- # we can run into a case where it's pre -> in -> post -> more infilters
25
- # in order to submit anything found during the infilters after the response has
26
- # been written we need to explicitly send them
27
+ # normally pre->in->post filters are applied however, in a streamed response we can run into a case
28
+ # where it's pre -> in -> post -> more infilters in order to submit anything found during the
29
+ # infilters after the response has been written we need to explicitly send them
27
30
  alias_method :cs__close, :close
28
31
  def close
29
32
  Contrast::Framework::Rails::Patch::ActionControllerLiveBuffer.send_messages
@@ -38,37 +38,39 @@ module Contrast
38
38
  instrumenting_module:
39
39
  'Contrast::Framework::Rails::Patch::RailsApplicationConfiguration')
40
40
  ])
41
- if RUBY_VERSION < '2.6.0'
42
- patches.merge([
43
- # TODO: RUBY-714 remove w/ EOL of 2.5
44
- #
45
- # @deprecated Everything past here is used for Rewriting and can
46
- # be removed once we no longer support 2.5.
47
- Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
48
- 'ActionController::Railties::Helper::ClassMethods',
49
- 'contrast/framework/rails/rewrite/action_controller_railties_helper_inherited',
50
- method_to_instrument: :inherited,
51
- instrumenting_module:
52
- 'Contrast::Framework::Rails::Rewrite::ActionControllerRailtiesHelperInherited'),
53
- Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
54
- 'ActiveRecord::AttributeMethods::Read::ClassMethods',
55
- 'contrast/framework/rails/rewrite/active_record_attribute_methods_read',
56
- instrumenting_module:
57
- 'Contrast::Framework::Rails::Rewrite::ActiveRecordAttributeMethodsRead'),
58
- Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
59
- 'ActiveRecord::Scoping::Named::ClassMethods',
60
- 'contrast/framework/rails/rewrite/active_record_named',
61
- instrumenting_module: 'Contrast::Framework::Rails::Rewrite::ActiveRecordNamed'),
62
- Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
63
- 'ActiveRecord::AttributeMethods::TimeZoneConversion::ClassMethods',
64
- 'contrast/framework/rails/rewrite/active_record_time_zone_inherited',
65
- method_to_instrument: :inherited,
66
- instrumenting_module:
67
- 'Contrast::Framework::Rails::Rewrite::ActiveRecordTimeZoneInherited')
68
- ])
69
- end
41
+ patches.merge(special_after_load_patches) if RUBY_VERSION < '2.6.0'
70
42
  patches
71
43
  end
44
+
45
+ def special_after_load_patches
46
+ [
47
+ # TODO: RUBY-714 remove w/ EOL of 2.5
48
+ #
49
+ # @deprecated Everything past here is used for Rewriting and can
50
+ # be removed once we no longer support 2.5.
51
+ Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
52
+ 'ActionController::Railties::Helper::ClassMethods',
53
+ 'contrast/framework/rails/rewrite/action_controller_railties_helper_inherited',
54
+ method_to_instrument: :inherited,
55
+ instrumenting_module:
56
+ 'Contrast::Framework::Rails::Rewrite::ActionControllerRailtiesHelperInherited'),
57
+ Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
58
+ 'ActiveRecord::AttributeMethods::Read::ClassMethods',
59
+ 'contrast/framework/rails/rewrite/active_record_attribute_methods_read',
60
+ instrumenting_module:
61
+ 'Contrast::Framework::Rails::Rewrite::ActiveRecordAttributeMethodsRead'),
62
+ Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
63
+ 'ActiveRecord::Scoping::Named::ClassMethods',
64
+ 'contrast/framework/rails/rewrite/active_record_named',
65
+ instrumenting_module: 'Contrast::Framework::Rails::Rewrite::ActiveRecordNamed'),
66
+ Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
67
+ 'ActiveRecord::AttributeMethods::TimeZoneConversion::ClassMethods',
68
+ 'contrast/framework/rails/rewrite/active_record_time_zone_inherited',
69
+ method_to_instrument: :inherited,
70
+ instrumenting_module:
71
+ 'Contrast::Framework::Rails::Rewrite::ActiveRecordTimeZoneInherited')
72
+ ]
73
+ end
72
74
  end
73
75
  end
74
76
  end
@@ -1,6 +1,8 @@
1
1
  # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'contrast/utils/exclude_key'
5
+
4
6
  module Contrast
5
7
  module Logger
6
8
  # Our decorator for the Ougai logger allowing for the logging of the
@@ -29,6 +31,8 @@ module Contrast
29
31
  loggable = ::Contrast::CONFIG.loggable
30
32
  info('Current configuration', configuration: loggable)
31
33
  env_keys = ENV.keys.select do |env_key|
34
+ next if Contrast::Utils::ExcludeKey.excludable? env_key.to_s
35
+
32
36
  env_key&.to_s&.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER)
33
37
  end
34
38
  env_items = env_keys.map { |env_key| Contrast::Utils::EnvConfigurationItem.new(env_key, nil) }
@@ -0,0 +1,129 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ module Assess
7
+ # This module will include all methods for some internal validations in the PropagationMethod module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ module PropagationMethodUtils
10
+ APPEND_ACTION = 'APPEND'
11
+ CENTER_ACTION = 'CENTER'
12
+ INSERT_ACTION = 'INSERT'
13
+ KEEP_ACTION = 'KEEP'
14
+ NEXT_ACTION = 'NEXT'
15
+ NOOP_ACTION = 'NOOP'
16
+ PREPEND_ACTION = 'PREPEND'
17
+ REPLACE_ACTION = 'REPLACE'
18
+ REMOVE_ACTION = 'REMOVE'
19
+ REVERSE_ACTION = 'REVERSE'
20
+ SPLAT_ACTION = 'SPLAT'
21
+ SPLIT_ACTION = 'SPLIT'
22
+ DB_WRITE_ACTION = 'DB_WRITE'
23
+ CUSTOM_ACTION = 'CUSTOM'
24
+
25
+ ZERO_LENGTH_ACTIONS = [DB_WRITE_ACTION, CUSTOM_ACTION, KEEP_ACTION, REPLACE_ACTION, SPLAT_ACTION].cs__freeze
26
+
27
+ PROPAGATION_ACTIONS = {
28
+ APPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Append,
29
+ CENTER_ACTION => Contrast::Agent::Assess::Policy::Propagator::Center,
30
+ INSERT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Insert,
31
+ KEEP_ACTION => Contrast::Agent::Assess::Policy::Propagator::Keep,
32
+ NEXT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Next,
33
+ NOOP_ACTION => nil,
34
+ PREPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Prepend,
35
+ REPLACE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Replace,
36
+ REMOVE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Remove,
37
+ REVERSE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Reverse,
38
+ SPLAT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Splat,
39
+ SPLIT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Split
40
+ }.cs__freeze
41
+
42
+ def determine_target propagation_node, ret, object, args
43
+ target = propagation_node.targets[0]
44
+ case target
45
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
46
+ object
47
+ when Contrast::Utils::ObjectShare::RETURN_KEY
48
+ ret
49
+ else
50
+ args[target]
51
+ end
52
+ end
53
+
54
+ # Custom actions tend to be the more complex of our propagations. Often, the method has to make decisions
55
+ # about the target based on the context with which the method was called. As such, defer determining if the
56
+ # target is valid to that method.
57
+ #
58
+ # In all other cases, a target is valid for propagation if it is not nil
59
+ #
60
+ # @param target [Object] the thing to which to propagate
61
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
62
+ # propagation event.
63
+ # @return [Boolean]
64
+ def valid_target? target, propagation_node
65
+ return true if propagation_node.action == CUSTOM_ACTION
66
+
67
+ !!target
68
+ end
69
+
70
+ # If the action required needs a length and the target does not have one, the length is not valid
71
+ #
72
+ # @param target [Object] the thing to which to propagate
73
+ # @param action [String] the name of the action taken during this propagation
74
+ # @return [Boolean]
75
+ def valid_length? target, action
76
+ return true if ZERO_LENGTH_ACTIONS.include?(action)
77
+
78
+ if Contrast::Utils::DuckUtils.quacks_to?(target, :length)
79
+ target.length != 0 # rubocop:disable Style/ZeroLengthPredicate
80
+ else
81
+ !target.to_s.empty?
82
+ end
83
+ end
84
+
85
+ # Before we do any work, we should check if we even need to. If the source and target of this patcher are
86
+ # not tracked, there's no need to do anything. A copy of nothing is still nothing.
87
+ #
88
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
89
+ # propagation event.
90
+ # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
91
+ # the invocation of the patched method.
92
+ # @param target [Object] the thing to which to propagate
93
+ # @return [Boolean]
94
+ def can_propagate? propagation_node, preshift, target
95
+ return false unless appropriate_target?(propagation_node, target)
96
+ return true if Contrast::Utils::Assess::TrackingUtil.tracked?(target)
97
+ return false unless preshift
98
+
99
+ propagation_node.sources.each do |source|
100
+ case source
101
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
102
+ return true if Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.object)
103
+ else
104
+ # has to be P, there's no ret source type (yet? ever?)
105
+ return true if preshift.args && Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.args[source])
106
+ end
107
+ end
108
+ false
109
+ end
110
+
111
+ # We cannot propagate to frozen things that have not been updated to work with our property tracking,
112
+ # unless they're duplicable and the return. We probably shouldn't propagate to frozen things at all, as
113
+ # they're supposed to be immutable, but third parties do jenky things, so allow it as long as it is safe to
114
+ # do.
115
+ #
116
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
117
+ # propagation event.
118
+ # @param target [Object] the Target to which to propagate.
119
+ # @return [Boolean] if the target can be propagated to
120
+ def appropriate_target? propagation_node, target
121
+ # special handle Returns b/c we can do unfreezing magic during propagation
122
+ return true if propagation_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
123
+
124
+ Contrast::Agent::Assess::Tracker.trackable?(target)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,142 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ module Assess
7
+ # This module will include all methods for some internal validations in the Tagged property module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ # This module includes simple methods for the tags like
10
+ # adding tags, getting tags, deleting tags and similar
11
+ module TaggedUtils
12
+ # Given a tag name and range object, add a new tag to this
13
+ # collection. If the given range touches an existing tag,
14
+ # we'll combine the two, adjusting the existing one and
15
+ # dropping this new one.
16
+ #
17
+ # @param label [String] the name of the tag
18
+ # @param range [Range] the Range that the tag covers, inclusive to
19
+ # exclusive
20
+ def add_tag label, range
21
+ length = range.end - range.begin
22
+ tag = Contrast::Agent::Assess::Tag.new(label, length, range.begin)
23
+ existing = fetch_tag(label)
24
+ tags[label] = Contrast::Utils::TagUtil.ordered_merge(existing, tag)
25
+ end
26
+
27
+ def set_tags label, tag_ranges
28
+ tags[label] = tag_ranges
29
+ end
30
+
31
+ # Returns a list of all current tags.
32
+ #
33
+ # @return [Hash<Contrast::Agent::Assess::Tag>]
34
+ def get_tags # rubocop:disable Naming/AccessorMethodName
35
+ return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
36
+
37
+ tags
38
+ end
39
+
40
+ # We'll use this as a helper method to retrieve tags from the hash.
41
+ # Because the hash auto-populates an empty array when we try to
42
+ # access a tag in it, we cannot use the [] method without side
43
+ # effect. To get around this, we'll use a fetch work around.
44
+ #
45
+ # @param label [Symbol] the label to look up
46
+ # @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
47
+ # that label
48
+ def fetch_tag label
49
+ get_tags.fetch(label, nil) if tracked?
50
+ end
51
+
52
+ # Remove all tags with a given label
53
+ def delete_tags label
54
+ tags.delete(label) if tracked?
55
+ end
56
+
57
+ # Reset the tag hash
58
+ def clear_tags
59
+ tags.clear if tracked?
60
+ end
61
+
62
+ # Returns a list of all current tag labels, most likely to be used for
63
+ # a splat operation
64
+ def tag_keys
65
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
66
+
67
+ tags.keys
68
+ end
69
+
70
+ # Calls merge to combine touching or overlapping tags
71
+ # Deletes empty tags
72
+ def cleanup_tags
73
+ return unless tracked?
74
+
75
+ Contrast::Utils::TagUtil.merge_tags(tags)
76
+ tags.delete_if { |_, value| value.empty? }
77
+ end
78
+
79
+ # Find all of the ranges that span a given index. This is used
80
+ # in propagation when we need to shift tags about. For instance, in
81
+ # the append method when we need to see if any tag at the end needs
82
+ # to be expanded out to the size of the new String.
83
+ #
84
+ # Note: Tags do not know their key, so this is only the range covered
85
+ #
86
+ # @param idx [Integer] the index to check for tags
87
+ # @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
88
+ # covering the given index. This is only the ranges, not the names.
89
+ def tags_at idx
90
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
91
+
92
+ at = []
93
+ tags.each_value do |tag_array|
94
+ tag_array.each do |tag|
95
+ if tag.covers?(idx)
96
+ at << tag
97
+ elsif tag.above?(idx)
98
+ break
99
+ end
100
+ end
101
+ end
102
+ at
103
+ end
104
+
105
+ # given a range, select all tags in that range the selected tags are
106
+ # shifted such that the start index of the new tag (0) aligns with
107
+ # the given start index in the range
108
+ #
109
+ # current tags: 5-15
110
+ # range : 5-10
111
+ # result : 0-05
112
+ #
113
+ # Note that we disable Lint/DuplicateBranch in this branch in order
114
+ # list out all tag range cases in the proper order to make this
115
+ # easier to understand
116
+ #
117
+ # @param range [Range] the span to check, inclusive to exclusive
118
+ # @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of
119
+ # key to tags
120
+ def tags_at_range range
121
+ return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
122
+
123
+ at = Hash.new { |h, k| h[k] = [] }
124
+ tags.each_pair do |key, value|
125
+ add = nil
126
+ value.each do |tag|
127
+ within_range = resize_to_range(tag, range)
128
+ if within_range
129
+ add ||= []
130
+ add << within_range
131
+ end
132
+ end
133
+ next unless add&.any?
134
+
135
+ at[key] = add
136
+ end
137
+ at
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,83 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ module Assess
7
+ # This module will include all methods for some internal validations in the SourceMethod module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ module SourceMethodUtils
10
+ # Safely duplicate the target, or return nil
11
+ #
12
+ # @param target [Object] the thing to check for duplication
13
+ def safe_dup target
14
+ target.dup
15
+ rescue StandardError => _e
16
+ nil
17
+ end
18
+
19
+ # Find the name of the source
20
+ #
21
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
22
+ # event
23
+ # @param object [Object] the Object on which the method was invoked
24
+ # @param ret [Object] the Return of the invoked method
25
+ # @param args [Array<Object>] the Arguments with which the method was invoked
26
+ # @return [String, nil] the human readable name of the target to which this source event applies, or nil if
27
+ # none provided by the node
28
+ def determine_source_name source_node, object, ret, *args
29
+ return source_node.get_property('dynamic_source_name') if source_node.type == 'UNTRUSTED_DATABASE'
30
+
31
+ source_node_source = source_node.sources[0]
32
+ case source_node_source
33
+ when nil
34
+ nil
35
+ when Contrast::Utils::ObjectShare::RETURN_KEY
36
+ ret
37
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
38
+ object
39
+ else
40
+ args[source_node_source]
41
+ end
42
+ end
43
+
44
+ # Determine if we should analyze this method invocation for a Source or not. We should if we have enough
45
+ # information to build the context of this invocation, we're not disabled, and we can't immediately
46
+ # determine the invocation was done safely.
47
+ #
48
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] the policy that applies to the
49
+ # method being called
50
+ # @param object [Object] the Object on which the method was invoked
51
+ # @param ret [Object] the Return of the invoked method
52
+ # @param args [Array<Object>] the Arguments with which the method was invoked
53
+ # @return [boolean] if the invocation of this method should be analyzed
54
+ def analyze? method_policy, object, ret, args
55
+ return false unless method_policy&.source_node
56
+ return false unless ::Contrast::ASSESS.enabled?
57
+ return false unless Contrast::Agent::REQUEST_TRACKER.current&.analyze_request?
58
+
59
+ !safe_invocation?(method_policy.source_node, object, ret, args)
60
+ end
61
+
62
+ # Determine if the method was invoked safely.
63
+ #
64
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the node to direct applying this source
65
+ # event
66
+ # @param _object [Object] the Object on which the method was invoked
67
+ # @param _ret [Object] the Return of the invoked method
68
+ # @param args [Array<Object>] the Arguments with which the method was invoked
69
+ # @return [boolean] if the invocation of this method was safe
70
+ def safe_invocation? source_node, _object, _ret, args
71
+ # According the the Rack Specification https://github.com/rack/rack/blob/master/SPEC.rdoc, any header
72
+ # from the Request will start with HTTP_. As such, only Headers with that key should be considered for
73
+ # tracking, as the others have come from the Framework or Middleware stashing in the ENV. Rails, for
74
+ # instance, uses action_dispatch. to store several values. Technically, you can't call
75
+ # Rack::Request#get_header without a parameter, and that parameter should be a String, but trust no one.
76
+ source_node.id == 'Assess:Source:Rack::Request::Env#get_header' &&
77
+ args&.any? &&
78
+ !args[0].to_s.start_with?('HTTP_')
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end