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.
- checksums.yaml +4 -4
- data/ext/cs__assess_module/cs__assess_module.c +48 -0
- data/ext/cs__assess_module/cs__assess_module.h +7 -0
- data/ext/cs__common/cs__common.c +24 -7
- data/ext/cs__common/cs__common.h +12 -2
- data/ext/cs__contrast_patch/cs__contrast_patch.c +48 -11
- data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -2
- data/ext/cs__os_information/cs__os_information.c +31 -0
- data/ext/cs__os_information/cs__os_information.h +7 -0
- data/ext/{cs__protect_kernel → cs__os_information}/extconf.rb +0 -0
- data/lib/contrast/agent/assess/contrast_event.rb +1 -1
- data/lib/contrast/agent/assess/contrast_object.rb +1 -4
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
- data/lib/contrast/agent/assess/policy/preshift.rb +25 -11
- data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
- data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -0
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +4 -4
- data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
- data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
- data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -107
- data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
- data/lib/contrast/agent/assess/property/tagged.rb +15 -132
- data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +2 -1
- data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
- data/lib/contrast/agent/middleware.rb +22 -0
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +0 -1
- data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
- data/lib/contrast/agent/patching/policy/patch.rb +37 -238
- data/lib/contrast/agent/patching/policy/patcher.rb +3 -42
- data/lib/contrast/agent/request.rb +5 -3
- data/lib/contrast/agent/request_context.rb +32 -11
- data/lib/contrast/agent/request_handler.rb +7 -3
- data/lib/contrast/agent/rule_set.rb +2 -4
- data/lib/contrast/agent/scope.rb +32 -20
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
- data/lib/contrast/agent/static_analysis.rb +4 -2
- data/lib/contrast/agent/telemetry.rb +129 -0
- data/lib/contrast/agent/telemetry_event.rb +34 -0
- data/lib/contrast/agent/thread_watcher.rb +43 -14
- data/lib/contrast/agent/tracepoint_hook.rb +11 -3
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +6 -1
- data/lib/contrast/components/api.rb +34 -0
- data/lib/contrast/components/app_context.rb +24 -0
- data/lib/contrast/components/assess.rb +7 -0
- data/lib/contrast/components/config.rb +90 -11
- data/lib/contrast/components/contrast_service.rb +6 -0
- data/lib/contrast/config/api_configuration.rb +22 -0
- data/lib/contrast/config/assess_configuration.rb +1 -0
- data/lib/contrast/config/env_variables.rb +25 -0
- data/lib/contrast/config/root_configuration.rb +1 -0
- data/lib/contrast/config/service_configuration.rb +2 -1
- data/lib/contrast/config.rb +1 -0
- data/lib/contrast/configuration.rb +3 -0
- data/lib/contrast/framework/manager.rb +14 -12
- data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
- data/lib/contrast/framework/rails/patch/support.rb +31 -29
- data/lib/contrast/logger/application.rb +4 -0
- data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
- data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
- data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
- data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
- data/lib/contrast/utils/class_util.rb +58 -44
- data/lib/contrast/utils/exclude_key.rb +20 -0
- data/lib/contrast/utils/io_util.rb +42 -34
- data/lib/contrast/utils/lru_cache.rb +45 -0
- data/lib/contrast/utils/metrics_hash.rb +59 -0
- data/lib/contrast/utils/os.rb +23 -0
- data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
- data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
- data/lib/contrast/utils/requests_client.rb +150 -0
- data/lib/contrast/utils/ruby_ast_rewriter.rb +1 -1
- data/lib/contrast/utils/telemetry.rb +77 -0
- data/lib/contrast/utils/telemetry_identifier.rb +137 -0
- data/lib/contrast.rb +19 -1
- data/resources/assess/policy.json +12 -6
- data/resources/deadzone/policy.json +86 -5
- data/ruby-agent.gemspec +2 -1
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +32 -14
- data/ext/cs__protect_kernel/cs__protect_kernel.c +0 -47
- data/ext/cs__protect_kernel/cs__protect_kernel.h +0 -12
- 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
|
-
#
|
14
|
-
#
|
15
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
55
|
-
|
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
|
-
|
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
|
-
#
|
75
|
-
#
|
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
|
-
#
|
80
|
-
# support for 2.6.X, we should remove
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
#
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|