contrast-agent 4.10.0 → 4.13.1

Sign up to get free protection for your applications and to get access to all the features.
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