contrast-agent 4.10.0 → 4.13.1

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 (87) 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 +24 -7
  5. data/ext/cs__common/cs__common.h +12 -2
  6. data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -11
  7. data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -2
  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__protect_kernel → cs__os_information}/extconf.rb +0 -0
  11. data/lib/contrast/agent/assess/contrast_event.rb +1 -1
  12. data/lib/contrast/agent/assess/contrast_object.rb +1 -4
  13. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
  14. data/lib/contrast/agent/assess/policy/preshift.rb +25 -11
  15. data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
  16. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  17. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -0
  18. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +4 -4
  19. data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
  20. data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
  21. data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -107
  22. data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
  23. data/lib/contrast/agent/assess/property/tagged.rb +15 -132
  24. data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
  25. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +2 -1
  26. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  27. data/lib/contrast/agent/middleware.rb +22 -0
  28. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +0 -1
  29. data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
  30. data/lib/contrast/agent/patching/policy/patch.rb +37 -238
  31. data/lib/contrast/agent/patching/policy/patcher.rb +3 -42
  32. data/lib/contrast/agent/request.rb +5 -3
  33. data/lib/contrast/agent/request_context.rb +32 -11
  34. data/lib/contrast/agent/request_handler.rb +7 -3
  35. data/lib/contrast/agent/rule_set.rb +2 -4
  36. data/lib/contrast/agent/scope.rb +32 -20
  37. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
  38. data/lib/contrast/agent/static_analysis.rb +4 -2
  39. data/lib/contrast/agent/telemetry.rb +129 -0
  40. data/lib/contrast/agent/telemetry_event.rb +34 -0
  41. data/lib/contrast/agent/thread_watcher.rb +43 -14
  42. data/lib/contrast/agent/tracepoint_hook.rb +11 -3
  43. data/lib/contrast/agent/version.rb +1 -1
  44. data/lib/contrast/agent.rb +6 -1
  45. data/lib/contrast/components/api.rb +34 -0
  46. data/lib/contrast/components/app_context.rb +24 -0
  47. data/lib/contrast/components/assess.rb +7 -0
  48. data/lib/contrast/components/config.rb +90 -11
  49. data/lib/contrast/components/contrast_service.rb +6 -0
  50. data/lib/contrast/config/api_configuration.rb +22 -0
  51. data/lib/contrast/config/assess_configuration.rb +1 -0
  52. data/lib/contrast/config/env_variables.rb +25 -0
  53. data/lib/contrast/config/root_configuration.rb +1 -0
  54. data/lib/contrast/config/service_configuration.rb +2 -1
  55. data/lib/contrast/config.rb +1 -0
  56. data/lib/contrast/configuration.rb +3 -0
  57. data/lib/contrast/framework/manager.rb +14 -12
  58. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  59. data/lib/contrast/framework/rails/patch/support.rb +31 -29
  60. data/lib/contrast/logger/application.rb +4 -0
  61. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  62. data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
  63. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  64. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  65. data/lib/contrast/utils/class_util.rb +58 -44
  66. data/lib/contrast/utils/exclude_key.rb +20 -0
  67. data/lib/contrast/utils/io_util.rb +42 -34
  68. data/lib/contrast/utils/lru_cache.rb +45 -0
  69. data/lib/contrast/utils/metrics_hash.rb +59 -0
  70. data/lib/contrast/utils/os.rb +23 -0
  71. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  72. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  73. data/lib/contrast/utils/requests_client.rb +150 -0
  74. data/lib/contrast/utils/ruby_ast_rewriter.rb +1 -1
  75. data/lib/contrast/utils/telemetry.rb +77 -0
  76. data/lib/contrast/utils/telemetry_identifier.rb +137 -0
  77. data/lib/contrast.rb +19 -1
  78. data/resources/assess/policy.json +12 -6
  79. data/resources/deadzone/policy.json +86 -5
  80. data/ruby-agent.gemspec +2 -1
  81. data/service_executables/VERSION +1 -1
  82. data/service_executables/linux/contrast-service +0 -0
  83. data/service_executables/mac/contrast-service +0 -0
  84. metadata +32 -14
  85. data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
  86. data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
  87. data/lib/contrast/extension/protect/kernel.rb +0 -29
@@ -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
@@ -0,0 +1,138 @@
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/appliers in the TriggerMethod module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ module TriggerMethodUtils
10
+ # A request is reportable if it is not from ActionController::Live
11
+ #
12
+ # @param env [Hash] the env of the Request
13
+ # @return [Boolean]
14
+ def reportable? env
15
+ !(defined?(ActionController::Live) &&
16
+ env &&
17
+ env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live))
18
+ end
19
+
20
+ # Find the request for this finding. This assumes, for now, that if there is an active request, then that
21
+ # is the request to report. Otherwise, we'll use the first request found in the events of the
22
+ # source_object.
23
+ #
24
+ # @param source [Object,nil] some Object used as the source of a trigger event
25
+ # @return [Contrast::Agent::Request,nil] the request from which the dataflow on the request originated.
26
+ def find_request source
27
+ return Contrast::Agent::REQUEST_TRACKER.current.request if Contrast::Agent::REQUEST_TRACKER.current
28
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties(source))
29
+
30
+ find_event_request(properties.event)
31
+ end
32
+
33
+ # Finds the first request along the left most tree of parent events
34
+ #
35
+ # @param event [Contrast::Agent::Assess::ContrastEvent|Contrast::Agent::Assess::Events::SourceEvent]
36
+ # @return [Contrast::Agent::Request, nil]
37
+ def find_event_request event
38
+ return event.request if event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent)
39
+
40
+ idx = 0
41
+ while idx <= event.parent_events.length
42
+ found = find_event_request(event.parent_events[idx])
43
+ return found if found
44
+
45
+ idx += 1
46
+ return event.request if event.request
47
+ end
48
+ return unless event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent)
49
+
50
+ event.request
51
+ end
52
+
53
+ # ===== APPLIERS =====
54
+ # This is our method that actually checks the taint on the object our trigger_node targets.
55
+ #
56
+ # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
57
+ # trigger event
58
+ # @param source [Object] the source of the Trigger Event
59
+ # @param object [Object] the Object on which the method was invoked
60
+ # @param ret [Object] the Return of the invoked method
61
+ # @param args [Array<Object>] the Arguments with which the method was invoked
62
+ def apply_trigger trigger_node, source, object, ret, *args
63
+ return unless trigger_node
64
+ return if trigger_node.rule_disabled?
65
+ return if trigger_node.dataflow? && source.nil?
66
+
67
+ if trigger_node.regexp_rule?
68
+ apply_regexp_rule(trigger_node, source, object, ret, *args)
69
+ elsif trigger_node.custom_trigger?
70
+ trigger_node.apply_custom_trigger(trigger_node, source, object, ret, *args)
71
+ elsif trigger_node.dataflow?
72
+ apply_dataflow_rule(trigger_node, source, object, ret, *args)
73
+ else # trigger rule - just calling the method is dangerous
74
+ build_finding(trigger_node, source, object, ret, *args)
75
+ end
76
+ rescue StandardError => e
77
+ logger.warn('Unable to apply trigger', e, node_id: trigger_node.id)
78
+ end
79
+
80
+ # This is our method that actually checks the taint on the object our trigger_node targets for our Regexp
81
+ # based rules.
82
+ #
83
+ # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
84
+ # trigger event
85
+ # @param source [Object] the source of the Trigger Event
86
+ # @param object [Object] the Object on which the method was invoked
87
+ # @param ret [Object] the Return of the invoked method
88
+ # @param args [Array<Object>] the Arguments with which the method was invoked
89
+ def apply_regexp_rule trigger_node, source, object, ret, *args
90
+ return unless source.is_a?(String)
91
+ return if trigger_node.good_value && source.match?(trigger_node.good_value)
92
+ return if trigger_node.bad_value && source !~ trigger_node.bad_value
93
+
94
+ build_finding(trigger_node, source, object, ret, *args)
95
+ end
96
+
97
+ # This is our method that actually checks the taint on the object our trigger_node targets for our Dataflow
98
+ # based rules.
99
+ #
100
+ # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
101
+ # trigger event
102
+ # @param source [Object] the source of the Trigger Event
103
+ # @param object [Object] the Object on which the method was invoked
104
+ # @param ret [Object] the Return of the invoked method
105
+ # @param args [Array<Object>] the Arguments with which the method was invoked
106
+ def apply_dataflow_rule trigger_node, source, object, ret, *args
107
+ return unless source
108
+
109
+ if Contrast::Agent::Assess::Tracker.trackable?(source)
110
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
111
+ return unless trigger_node.violated?(source)
112
+
113
+ build_finding(trigger_node, source, object, ret, *args)
114
+ elsif Contrast::Utils::DuckUtils.iterable_hash?(source)
115
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
116
+
117
+ source.each_pair do |key, value|
118
+ apply_dataflow_rule(trigger_node, key, object, ret, *args)
119
+ apply_dataflow_rule(trigger_node, value, object, ret, *args)
120
+ end
121
+ elsif Contrast::Utils::DuckUtils.iterable_enumerable?(source)
122
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
123
+
124
+ source.each do |value|
125
+ apply_dataflow_rule(trigger_node, value, object, ret, *args)
126
+ end
127
+ else
128
+ logger.debug('Trigger source is untrackable. Unable to inspect.',
129
+ node_id: trigger_node.id,
130
+ source_id: source.__id__,
131
+ source_type: source.cs__class.cs__name,
132
+ frozen: source.cs__frozen?)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -3,21 +3,21 @@
3
3
 
4
4
  require 'contrast/extension/module'
5
5
  require 'contrast/utils/object_share'
6
+ require 'contrast/utils/lru_cache'
6
7
 
7
8
  module Contrast
8
9
  module Utils
9
10
  # Utility methods for exploring the complete space of Objects
10
11
  class ClassUtil
12
+ @lru_cache = LRUCache.new(300)
13
+ @string_cache = LRUCache.new(300)
11
14
  class << self
12
- # some classes have had things prepended to them, like Marshal in Rails
13
- # 5 and higher. Their ActiveSupport::MarshalWithAutoloading will break
14
- # our alias patching approach, as will any other prepend on something
15
- # that we touch. Prepend and Alias are inherently incompatible monkey
16
- # patching approaches. As such, we need to know if something has been
17
- # prepended to.
15
+ # some classes have had things prepended to them, like Marshal in Rails 5 and higher. Their
16
+ # ActiveSupport::MarshalWithAutoloading will break our alias patching approach, as will any other prepend on
17
+ # something that we touch. Prepend and Alias are inherently incompatible monkey patching approaches. As such,
18
+ # we need to know if something has been prepended to.
18
19
  #
19
- # @param mod [Module] the Module to check to see if it has had something
20
- # prepended
20
+ # @param mod [Module] the Module to check to see if it has had something prepended
21
21
  # @param ancestors [Array<Module>] the array of ancestors for the mod
22
22
  # @return [Boolean] if the mod has been prepended or not
23
23
  def prepended? mod, ancestors = nil
@@ -25,8 +25,13 @@ module Contrast
25
25
  ancestors[0] != mod
26
26
  end
27
27
 
28
- # return true if the given method is overwritten by one of the ancestors
29
- # in the ancestor change that comes before the given module
28
+ # return true if the given method is overwritten by one of the ancestors in the ancestor change that comes
29
+ # before the given module
30
+ #
31
+ # @param mod [Module] the Module to check to see if it has had something prepended
32
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] the policy that holds the method we
33
+ # need to check
34
+ # @return [Boolean] if this method specifically was prepended
30
35
  def prepended_method? mod, method_policy
31
36
  target_module = determine_target_class mod, method_policy.instance_method
32
37
  ancestors = target_module.ancestors
@@ -41,44 +46,49 @@ module Contrast
41
46
  false
42
47
  end
43
48
 
44
- # Return a String representing the object invoking this method in the
45
- # form expected by our dataflow events.
49
+ # Return a String representing the object invoking this method in the form expected by our dataflow events.
50
+ # After implementing the LRU Cache, we firstly need to check if already had that object cached and if we have
51
+ # it - we can return it directly; otherwise we'll calculate and store the result before returning.
52
+ #
53
+ # TODO: RUBY-1327
54
+ # Once we move to 2.7+, we can combine the caches using ID b/c the memory location stops being the id
46
55
  #
47
56
  # @param object [Object, nil] the entity to convert to a String
48
57
  # @return [String] the human readable form of the String, as defined by
49
58
  # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md
50
59
  def to_contrast_string object
51
- # Only treat object like a string if it actually is a string
52
- # some subclasses of String override string methods we depend on
60
+ # Only treat object like a string if it actually is a string+ some subclasses of String override string
61
+ # methods we depend on
53
62
  if object.cs__class == String
54
- cached = to_cached_string(object)
55
- return cached if cached
56
-
57
- object.dup
58
- elsif object.nil?
59
- Contrast::Utils::ObjectShare::NIL_STRING
60
- elsif object.cs__is_a?(Symbol)
61
- ":#{ object }"
62
- elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
63
- "#{ object.cs__name }@#{ object.__id__ }"
64
- elsif object.cs__is_a?(Regexp)
65
- object.source
66
- elsif use_to_s?(object)
67
- object.to_s
63
+ return @string_cache[object] if @string_cache.key? object
64
+
65
+ @string_cache[object] = to_cached_string(object) || object.dup
68
66
  else
69
- "#{ object.cs__class.cs__name }@#{ object.__id__ }"
67
+ return @lru_cache[object.__id__] if @lru_cache.key? object.__id__
68
+
69
+ @lru_cache[object.__id__] = if object.nil?
70
+ Contrast::Utils::ObjectShare::NIL_STRING
71
+ elsif object.cs__is_a?(Symbol)
72
+ ":#{ object }"
73
+ elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
74
+ "#{ object.cs__name }@#{ object.__id__ }"
75
+ elsif object.cs__is_a?(Regexp)
76
+ object.source
77
+ elsif use_to_s?(object)
78
+ object.to_s
79
+ else
80
+ "#{ object.cs__class.cs__name }@#{ object.__id__ }"
81
+ end
70
82
  end
71
83
  end
72
84
 
73
- # The method const_defined? can cause autoload, which is bad for us.
74
- # The method autoload? doesn't traverse namespaces. This method lets us
75
- # provide a constant, as a String, and parse it to determine if it has
76
- # been truly truly defined, meaning it existed before this method was
77
- # invoked, not as a result of it.
85
+ # The method const_defined? can cause autoload, which is bad for us. The method autoload? doesn't traverse
86
+ # namespaces. This method lets us provide a constant, as a String, and parse it to determine if it has been
87
+ # truly truly defined, meaning it existed before this method was invoked, not as a result of it.
78
88
  #
79
- # This is required to handle a bug in Ruby prior to 2.7.0. When we drop
80
- # support for 2.6.X, we should remove this code.
81
- # https://bugs.ruby-lang.org/issues/10741
89
+ # TODO: RUBY-1326
90
+ # This is required to handle a bug in Ruby prior to 2.7.0. When we drop support for 2.6.X, we should remove
91
+ # this code. https://bugs.ruby-lang.org/issues/10741
82
92
  # @param name [String] the name of the constant to look up
83
93
  # @return [Boolean]
84
94
  def truly_defined? name
@@ -101,7 +111,8 @@ module Contrast
101
111
  private
102
112
 
103
113
  # Some objects have nice to_s that we can use to make them human readable. If they do, we should leverage them.
104
- # We used to do this by default, but this opened us up to danger, so we're instead using an allow list approach.
114
+ # We used to do this by default, but this opened us up to danger, so we're instead using an allow list
115
+ # approach.
105
116
  #
106
117
  # @param object [Object] something that may have a safe to_s method
107
118
  # @return [Boolean] if we should invoke to_s to represent the object
@@ -112,6 +123,11 @@ module Contrast
112
123
  false
113
124
  end
114
125
 
126
+ # Find the target class based on the instance, or module, provided. If a module, return it.
127
+ #
128
+ # @param mod [Module] the Module, or instance of a Module, that we need to check
129
+ # @param is_instance [Boolean] is the object provided an instance of a class, requiring lookup by class
130
+ # @return [Module]
115
131
  def determine_target_class mod, is_instance
116
132
  return mod if mod.singleton_class?
117
133
 
@@ -120,13 +136,11 @@ module Contrast
120
136
  mod
121
137
  end
122
138
 
123
- # If the String matches a common String in our ObjectShare, return that
124
- # rather that for use as the representation of the String rather than
125
- # forcing a duplication of the String.
139
+ # If the String matches a common String in our ObjectShare, return that rather that for use as the
140
+ # representation of the String rather than forcing a duplication of the String.
126
141
  #
127
- # @param string [String] some string of which we want a Contrast
128
- # representation.
129
- # @return [String,nil] the ObjectShare version of the String or nil
142
+ # @param string [String] some string of which we want a Contrast representation.
143
+ # @return [String, nil] the ObjectShare version of the String or nil
130
144
  def to_cached_string string
131
145
  return Contrast::Utils::ObjectShare::EMPTY_STRING if string.empty?
132
146
  return Contrast::Utils::ObjectShare::SLASH if string == Contrast::Utils::ObjectShare::SLASH
@@ -0,0 +1,20 @@
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
+ # Determine if configuration keys is excluded from logging
7
+ module ExcludeKey
8
+ EXCLUDE_FROM_LOG = %w[api api_key url service_key user_name].cs__freeze
9
+ class << self
10
+ # Check if a config key can be logged or not
11
+ #
12
+ # @param key [String] key to check
13
+ # @return[Boolean] true | false
14
+ def excludable? key
15
+ EXCLUDE_FROM_LOG.any? { |exclude_key| key.include? exclude_key }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -9,40 +9,48 @@ module Contrast
9
9
  module IOUtil
10
10
  extend Contrast::Components::Logger::InstanceMethods
11
11
 
12
- # We're only going to call rewind on things that we believe are safe to
13
- # call it on. This method white lists those cases and returns false in
14
- # all others.
15
- def self.should_rewind? potential_io
16
- return true if potential_io.is_a?(StringIO)
17
- return false unless io?(potential_io)
18
-
19
- should_rewind_io?(potential_io)
20
- rescue StandardError => e
21
- logger.debug('Encountered an issue determining if rewindable', e, module: potential_io.cs__class.cs__name)
22
- false
23
- end
24
-
25
- # IO cannot be used with streams such as pipes, ttys, and sockets.
26
- def self.should_rewind_io? io
27
- return false if io.tty?
28
-
29
- status = io.stat
30
- return false unless status
31
- return false if status.pipe?
32
- return false if status.socket?
33
-
34
- true
35
- end
36
-
37
- # A class is IO if it is a decedent or delegate of IO
38
- def self.io? object
39
- return true if object.is_a?(IO)
40
-
41
- # DelegateClass, which is a Delegator, defines __getobj__ as a way to
42
- # get the object that the class wraps around (delegates to)
43
- return false unless object.is_a?(Delegator)
44
-
45
- object.__getobj__.is_a?(IO)
12
+ class << self
13
+ # We're only going to call rewind on things that we believe are safe to
14
+ # call it on. This method white lists those cases and returns false in
15
+ # all others.
16
+ def should_rewind? potential_io
17
+ return true if potential_io.is_a?(StringIO)
18
+ return false unless io?(potential_io)
19
+
20
+ should_rewind_io?(potential_io)
21
+ rescue StandardError => e
22
+ logger.debug('Encountered an issue determining if rewindable', e, module: potential_io.cs__class.cs__name)
23
+ false
24
+ end
25
+
26
+ # A class is IO if it is a decedent or delegate of IO
27
+ def io? object
28
+ return true if object.is_a?(IO)
29
+
30
+ # DelegateClass, which is a Delegator, defines __getobj__ as a way to
31
+ # get the object that the class wraps around (delegates to)
32
+ return false unless object.is_a?(Delegator)
33
+
34
+ object.__getobj__.is_a?(IO)
35
+ end
36
+
37
+ private
38
+
39
+ # IO rewind cannot be used with streams such as pipes, ttys, and sockets or for ones which have been closed.
40
+ #
41
+ # @param io [IO] the input to check for the ability to rewind
42
+ # @return [Boolean] if the given IO can be rewound
43
+ def should_rewind_io? io
44
+ return false if io.closed?
45
+ return false if io.tty?
46
+
47
+ status = io.stat
48
+ return false unless status
49
+ return false if status.pipe?
50
+ return false if status.socket?
51
+
52
+ true
53
+ end
46
54
  end
47
55
  end
48
56
  end