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.
- 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
|