contrast-agent 4.11.0 → 4.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- 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 -1
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
- data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
- data/lib/contrast/agent/assess/policy/preshift.rb +19 -6
- 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/center.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -0
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
- data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
- data/lib/contrast/agent/assess/policy/trigger_method.rb +45 -110
- data/lib/contrast/agent/assess/policy/trigger_node.rb +62 -21
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
- data/lib/contrast/agent/assess/property/tagged.rb +66 -189
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
- data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +1 -0
- data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
- data/lib/contrast/agent/middleware.rb +14 -62
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +0 -1
- data/lib/contrast/agent/patching/policy/method_policy.rb +3 -44
- data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
- data/lib/contrast/agent/patching/policy/patch.rb +37 -238
- data/lib/contrast/agent/patching/policy/patcher.rb +15 -50
- data/lib/contrast/agent/reporting/report.rb +21 -0
- data/lib/contrast/agent/reporting/reporter.rb +142 -0
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
- data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
- data/lib/contrast/agent/request.rb +2 -81
- data/lib/contrast/agent/request_context.rb +18 -126
- data/lib/contrast/agent/request_context_extend.rb +138 -0
- data/lib/contrast/agent/request_handler.rb +7 -3
- data/lib/contrast/agent/response.rb +2 -73
- data/lib/contrast/agent/rule_set.rb +2 -4
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +94 -0
- data/lib/contrast/agent/static_analysis.rb +5 -3
- data/lib/contrast/agent/telemetry.rb +137 -0
- data/lib/contrast/agent/telemetry_event.rb +33 -0
- data/lib/contrast/agent/thread_watcher.rb +66 -11
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +21 -1
- data/lib/contrast/api/communication/connection_status.rb +10 -7
- data/lib/contrast/api/communication/messaging_queue.rb +37 -3
- data/lib/contrast/api/communication/response_processor.rb +15 -8
- data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
- data/lib/contrast/api/communication/socket.rb +6 -8
- data/lib/contrast/api/communication/socket_client.rb +29 -12
- data/lib/contrast/api/communication/speedracer.rb +37 -1
- data/lib/contrast/api/communication/tcp_socket.rb +4 -3
- data/lib/contrast/api/communication/unix_socket.rb +1 -0
- data/lib/contrast/api/decorators/finding.rb +45 -0
- data/lib/contrast/components/api.rb +90 -0
- data/lib/contrast/components/app_context.rb +10 -41
- data/lib/contrast/components/app_context_extend.rb +78 -0
- data/lib/contrast/components/assess.rb +7 -0
- data/lib/contrast/components/base.rb +23 -0
- data/lib/contrast/components/config.rb +92 -13
- data/lib/contrast/components/contrast_service.rb +11 -0
- data/lib/contrast/components/sampling.rb +2 -2
- data/lib/contrast/config/agent_configuration.rb +1 -1
- data/lib/contrast/config/api_configuration.rb +27 -0
- data/lib/contrast/config/api_proxy_configuration.rb +14 -0
- data/lib/contrast/config/application_configuration.rb +2 -3
- data/lib/contrast/config/assess_configuration.rb +3 -2
- data/lib/contrast/config/base_configuration.rb +17 -28
- data/lib/contrast/config/certification_configuration.rb +15 -0
- data/lib/contrast/config/env_variables.rb +18 -0
- data/lib/contrast/config/heap_dump_configuration.rb +6 -6
- data/lib/contrast/config/inventory_configuration.rb +1 -5
- data/lib/contrast/config/protect_rule_configuration.rb +1 -1
- data/lib/contrast/config/request_audit_configuration.rb +18 -0
- data/lib/contrast/config/root_configuration.rb +1 -0
- data/lib/contrast/config/ruby_configuration.rb +6 -6
- data/lib/contrast/config/service_configuration.rb +2 -2
- data/lib/contrast/config.rb +1 -1
- data/lib/contrast/configuration.rb +4 -2
- data/lib/contrast/extension/assess/array.rb +5 -7
- data/lib/contrast/framework/manager.rb +22 -44
- data/lib/contrast/framework/manager_extend.rb +50 -0
- 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/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/sinatra/support.rb +2 -1
- data/lib/contrast/logger/application.rb +4 -0
- data/lib/contrast/logger/log.rb +8 -103
- data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
- data/lib/contrast/utils/assess/property/tagged_utils.rb +165 -0
- data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
- data/lib/contrast/utils/assess/tracking_util.rb +20 -15
- data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
- data/lib/contrast/utils/class_util.rb +65 -54
- data/lib/contrast/utils/exclude_key.rb +20 -0
- data/lib/contrast/utils/findings.rb +62 -0
- data/lib/contrast/utils/hash_digest.rb +10 -73
- data/lib/contrast/utils/hash_digest_extend.rb +86 -0
- data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
- data/lib/contrast/utils/heap_dump_util.rb +2 -65
- data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
- data/lib/contrast/utils/io_util.rb +1 -1
- data/lib/contrast/utils/log_utils.rb +108 -0
- data/lib/contrast/utils/lru_cache.rb +4 -2
- data/lib/contrast/utils/metrics_hash.rb +59 -0
- data/lib/contrast/utils/middleware_utils.rb +87 -0
- data/lib/contrast/utils/net_http_base.rb +158 -0
- data/lib/contrast/utils/object_share.rb +1 -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/request_utils.rb +88 -0
- data/lib/contrast/utils/response_utils.rb +97 -0
- data/lib/contrast/utils/substitution_utils.rb +167 -0
- data/lib/contrast/utils/tag_util.rb +9 -9
- data/lib/contrast/utils/telemetry.rb +79 -0
- data/lib/contrast/utils/telemetry_client.rb +90 -0
- data/lib/contrast/utils/telemetry_identifier.rb +130 -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 +7 -6
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +68 -26
- 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/config/default_value.rb +0 -17
- data/lib/contrast/extension/protect/kernel.rb +0 -29
@@ -0,0 +1,165 @@
|
|
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
|
+
# Is the given tag present?
|
13
|
+
# Used in testing, so found by `be_tagged`, if you're grepping for it
|
14
|
+
#
|
15
|
+
# @param label [Symbol] the tag to check for
|
16
|
+
# @return [Boolean]
|
17
|
+
def tagged? label
|
18
|
+
tracked? && tags.key?(label)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Similar to #tracked?, but limited to a given range.
|
22
|
+
#
|
23
|
+
# @param start [Integer] the inclusive start index to check.
|
24
|
+
# @param finish [Integer] the exclusive end index to check.
|
25
|
+
# @return [Boolean]
|
26
|
+
def any_tags_between? start, finish
|
27
|
+
return false unless tracked?
|
28
|
+
|
29
|
+
tags.each_value do |tag_array|
|
30
|
+
return true if tag_array.any? { |tag| tag.overlaps?(start, finish) }
|
31
|
+
end
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
# Given a tag name and range object, add a new tag to this
|
36
|
+
# collection. If the given range touches an existing tag,
|
37
|
+
# we'll combine the two, adjusting the existing one and
|
38
|
+
# dropping this new one.
|
39
|
+
#
|
40
|
+
# @param label [String] the name of the tag
|
41
|
+
# @param range [Range] the Range that the tag covers, inclusive to
|
42
|
+
# exclusive
|
43
|
+
def add_tag label, range
|
44
|
+
length = range.end - range.begin
|
45
|
+
tag = Contrast::Agent::Assess::Tag.new(label, length, range.begin)
|
46
|
+
existing = fetch_tag(label)
|
47
|
+
tags[label] = Contrast::Utils::TagUtil.ordered_merge(existing, tag)
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_tags label, tag_ranges
|
51
|
+
tags[label] = tag_ranges
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a list of all current tags.
|
55
|
+
#
|
56
|
+
# @return [Hash<Contrast::Agent::Assess::Tag>]
|
57
|
+
def get_tags # rubocop:disable Naming/AccessorMethodName
|
58
|
+
return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
|
59
|
+
|
60
|
+
tags
|
61
|
+
end
|
62
|
+
|
63
|
+
# We'll use this as a helper method to retrieve tags from the hash.
|
64
|
+
# Because the hash auto-populates an empty array when we try to
|
65
|
+
# access a tag in it, we cannot use the [] method without side
|
66
|
+
# effect. To get around this, we'll use a fetch work around.
|
67
|
+
#
|
68
|
+
# @param label [Symbol] the label to look up
|
69
|
+
# @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
|
70
|
+
# that label
|
71
|
+
def fetch_tag label
|
72
|
+
get_tags.fetch(label, nil) if tracked?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove all tags with a given label
|
76
|
+
def delete_tags label
|
77
|
+
tags.delete(label) if tracked?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Reset the tag hash
|
81
|
+
def clear_tags
|
82
|
+
tags.clear if tracked?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a list of all current tag labels, most likely to be used for
|
86
|
+
# a splat operation
|
87
|
+
def tag_keys
|
88
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
|
89
|
+
|
90
|
+
tags.keys
|
91
|
+
end
|
92
|
+
|
93
|
+
# Calls merge to combine touching or overlapping tags
|
94
|
+
# Deletes empty tags
|
95
|
+
def cleanup_tags
|
96
|
+
return unless tracked?
|
97
|
+
|
98
|
+
Contrast::Utils::TagUtil.merge_tags(tags)
|
99
|
+
tags.delete_if { |_, value| value.empty? }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Find all of the ranges that span a given index. This is used
|
103
|
+
# in propagation when we need to shift tags about. For instance, in
|
104
|
+
# the append method when we need to see if any tag at the end needs
|
105
|
+
# to be expanded out to the size of the new String.
|
106
|
+
#
|
107
|
+
# Note: Tags do not know their key, so this is only the range covered
|
108
|
+
#
|
109
|
+
# @param idx [Integer] the index to check for tags
|
110
|
+
# @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
|
111
|
+
# covering the given index. This is only the ranges, not the names.
|
112
|
+
def tags_at idx
|
113
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked?
|
114
|
+
|
115
|
+
at = []
|
116
|
+
tags.each_value do |tag_array|
|
117
|
+
tag_array.each do |tag|
|
118
|
+
if tag.covers?(idx)
|
119
|
+
at << tag
|
120
|
+
elsif tag.above?(idx)
|
121
|
+
break
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
at
|
126
|
+
end
|
127
|
+
|
128
|
+
# given a range, select all tags in that range the selected tags are
|
129
|
+
# shifted such that the start index of the new tag (0) aligns with
|
130
|
+
# the given start index in the range
|
131
|
+
#
|
132
|
+
# current tags: 5-15
|
133
|
+
# range : 5-10
|
134
|
+
# result : 0-05
|
135
|
+
#
|
136
|
+
# Note that we disable Lint/DuplicateBranch in this branch in order
|
137
|
+
# list out all tag range cases in the proper order to make this
|
138
|
+
# easier to understand
|
139
|
+
#
|
140
|
+
# @param range [Range] the span to check, inclusive to exclusive
|
141
|
+
# @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of
|
142
|
+
# key to tags
|
143
|
+
def tags_at_range range
|
144
|
+
return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked?
|
145
|
+
|
146
|
+
at = Hash.new { |h, k| h[k] = [] }
|
147
|
+
tags.each_pair do |key, value|
|
148
|
+
add = nil
|
149
|
+
value.each do |tag|
|
150
|
+
within_range = resize_to_range(tag, range)
|
151
|
+
if within_range
|
152
|
+
add ||= []
|
153
|
+
add << within_range
|
154
|
+
end
|
155
|
+
end
|
156
|
+
next unless add&.any?
|
157
|
+
|
158
|
+
at[key] = add
|
159
|
+
end
|
160
|
+
at
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
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
|
@@ -50,14 +50,9 @@ module Contrast
|
|
50
50
|
|
51
51
|
idx += 1
|
52
52
|
if Contrast::Utils::DuckUtils.iterable_hash?(obj)
|
53
|
-
obj
|
54
|
-
return true if _tracked?(k, idx) || _tracked?(v, idx)
|
55
|
-
end
|
56
|
-
false
|
53
|
+
handle_hash obj, idx
|
57
54
|
elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj)
|
58
|
-
obj
|
59
|
-
_tracked?(ele, idx) unless obj == ele
|
60
|
-
end
|
55
|
+
handle_enumerable obj, idx
|
61
56
|
else
|
62
57
|
Contrast::Agent::Assess::Tracker.tracked?(obj)
|
63
58
|
end
|
@@ -84,15 +79,9 @@ module Contrast
|
|
84
79
|
|
85
80
|
idx += 1
|
86
81
|
if Contrast::Utils::DuckUtils.iterable_hash?(obj)
|
87
|
-
obj
|
88
|
-
return true if _trackable?(k, idx)
|
89
|
-
return true if _trackable?(v, idx)
|
90
|
-
end
|
91
|
-
false
|
82
|
+
handle_hash obj, idx
|
92
83
|
elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj)
|
93
|
-
obj
|
94
|
-
_trackable?(ele, idx) unless obj == ele
|
95
|
-
end
|
84
|
+
handle_enumerable obj, idx
|
96
85
|
else
|
97
86
|
Contrast::Agent::Assess::Tracker.trackable?(obj)
|
98
87
|
end
|
@@ -103,6 +92,22 @@ module Contrast
|
|
103
92
|
logger.warn('Failed to determine trackable', e, module: obj.cs__class)
|
104
93
|
false
|
105
94
|
end
|
95
|
+
|
96
|
+
def handle_hash obj, idx
|
97
|
+
caller_method = caller(1..1).first[/`.*'/][1..-2].to_sym
|
98
|
+
obj.each_pair do |k, v|
|
99
|
+
return true if send(caller_method, k, idx)
|
100
|
+
return true if send(caller_method, v, idx)
|
101
|
+
end
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
def handle_enumerable obj, idx
|
106
|
+
caller_method = caller(1..1).first[/`.*'/][1..-2].to_sym
|
107
|
+
obj.any? do |ele|
|
108
|
+
send(caller_method, ele, idx) unless obj == ele
|
109
|
+
end
|
110
|
+
end
|
106
111
|
end
|
107
112
|
end
|
108
113
|
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 # rubocop:disable Metrics/PerceivedComplexity
|
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
|
@@ -9,17 +9,15 @@ module Contrast
|
|
9
9
|
module Utils
|
10
10
|
# Utility methods for exploring the complete space of Objects
|
11
11
|
class ClassUtil
|
12
|
-
@lru_cache = LRUCache.new
|
12
|
+
@lru_cache = LRUCache.new(300)
|
13
|
+
@string_cache = LRUCache.new(300)
|
13
14
|
class << self
|
14
|
-
# some classes have had things prepended to them, like Marshal in Rails
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# patching approaches. As such, we need to know if something has been
|
19
|
-
# 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.
|
20
19
|
#
|
21
|
-
# @param mod [Module] the Module to check to see if it has had something
|
22
|
-
# prepended
|
20
|
+
# @param mod [Module] the Module to check to see if it has had something prepended
|
23
21
|
# @param ancestors [Array<Module>] the array of ancestors for the mod
|
24
22
|
# @return [Boolean] if the mod has been prepended or not
|
25
23
|
def prepended? mod, ancestors = nil
|
@@ -27,8 +25,13 @@ module Contrast
|
|
27
25
|
ancestors[0] != mod
|
28
26
|
end
|
29
27
|
|
30
|
-
# return true if the given method is overwritten by one of the ancestors
|
31
|
-
#
|
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
|
32
35
|
def prepended_method? mod, method_policy
|
33
36
|
target_module = determine_target_class mod, method_policy.instance_method
|
34
37
|
ancestors = target_module.ancestors
|
@@ -43,49 +46,53 @@ module Contrast
|
|
43
46
|
false
|
44
47
|
end
|
45
48
|
|
46
|
-
# Return a String representing the object invoking this method in the
|
47
|
-
#
|
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
|
48
55
|
#
|
49
56
|
# @param object [Object, nil] the entity to convert to a String
|
50
|
-
# @return [String] the human readable form of the String, as defined by
|
57
|
+
# @return [String, Object] the human readable form of the String, as defined by
|
51
58
|
# https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md
|
52
|
-
|
53
59
|
def to_contrast_string object
|
54
|
-
#
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
elsif object.nil?
|
66
|
-
Contrast::Utils::ObjectShare::NIL_STRING
|
67
|
-
elsif object.cs__is_a?(Symbol)
|
68
|
-
":#{ object }"
|
69
|
-
elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
|
70
|
-
"#{ object.cs__name }@#{ object.__id__ }"
|
71
|
-
elsif object.cs__is_a?(Regexp)
|
72
|
-
object.source
|
73
|
-
elsif use_to_s?(object)
|
74
|
-
object.to_s
|
75
|
-
else
|
76
|
-
"#{ object.cs__class.cs__name }@#{ object.__id__ }"
|
77
|
-
end
|
60
|
+
# Only treat object like a string if it actually is a string+ some subclasses of String override string
|
61
|
+
# methods we depend on
|
62
|
+
if object.cs__class == String
|
63
|
+
return @string_cache[object] if @string_cache.key? object
|
64
|
+
|
65
|
+
@string_cache[object] = to_cached_string(object) || object.dup
|
66
|
+
else
|
67
|
+
return @lru_cache[object.__id__] if @lru_cache.key? object.__id__
|
68
|
+
|
69
|
+
@lru_cache[object.__id__] = convert_object object
|
70
|
+
end
|
78
71
|
end
|
79
72
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
73
|
+
def convert_object object
|
74
|
+
if object.nil?
|
75
|
+
Contrast::Utils::ObjectShare::NIL_STRING
|
76
|
+
elsif object.cs__is_a?(Symbol)
|
77
|
+
":#{ object }"
|
78
|
+
elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
|
79
|
+
"#{ object.cs__name }@#{ object.__id__ }"
|
80
|
+
elsif object.cs__is_a?(Regexp)
|
81
|
+
object.source
|
82
|
+
elsif use_to_s?(object)
|
83
|
+
object.to_s
|
84
|
+
else
|
85
|
+
"#{ object.cs__class.cs__name }@#{ object.__id__ }"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# The method const_defined? can cause autoload, which is bad for us. The method autoload? doesn't traverse
|
90
|
+
# namespaces. This method lets us provide a constant, as a String, and parse it to determine if it has been
|
91
|
+
# truly truly defined, meaning it existed before this method was invoked, not as a result of it.
|
85
92
|
#
|
86
|
-
#
|
87
|
-
# support for 2.6.X, we should remove
|
88
|
-
# https://bugs.ruby-lang.org/issues/10741
|
93
|
+
# TODO: RUBY-1326
|
94
|
+
# 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
|
95
|
+
# this code. https://bugs.ruby-lang.org/issues/10741
|
89
96
|
# @param name [String] the name of the constant to look up
|
90
97
|
# @return [Boolean]
|
91
98
|
def truly_defined? name
|
@@ -108,7 +115,8 @@ module Contrast
|
|
108
115
|
private
|
109
116
|
|
110
117
|
# Some objects have nice to_s that we can use to make them human readable. If they do, we should leverage them.
|
111
|
-
# We used to do this by default, but this opened us up to danger, so we're instead using an allow list
|
118
|
+
# We used to do this by default, but this opened us up to danger, so we're instead using an allow list
|
119
|
+
# approach.
|
112
120
|
#
|
113
121
|
# @param object [Object] something that may have a safe to_s method
|
114
122
|
# @return [Boolean] if we should invoke to_s to represent the object
|
@@ -119,6 +127,11 @@ module Contrast
|
|
119
127
|
false
|
120
128
|
end
|
121
129
|
|
130
|
+
# Find the target class based on the instance, or module, provided. If a module, return it.
|
131
|
+
#
|
132
|
+
# @param mod [Module] the Module, or instance of a Module, that we need to check
|
133
|
+
# @param is_instance [Boolean] is the object provided an instance of a class, requiring lookup by class
|
134
|
+
# @return [Module]
|
122
135
|
def determine_target_class mod, is_instance
|
123
136
|
return mod if mod.singleton_class?
|
124
137
|
|
@@ -127,13 +140,11 @@ module Contrast
|
|
127
140
|
mod
|
128
141
|
end
|
129
142
|
|
130
|
-
# If the String matches a common String in our ObjectShare, return that
|
131
|
-
#
|
132
|
-
# forcing a duplication of the String.
|
143
|
+
# If the String matches a common String in our ObjectShare, return that rather that for use as the
|
144
|
+
# representation of the String rather than forcing a duplication of the String.
|
133
145
|
#
|
134
|
-
# @param string [String] some string of which we want a Contrast
|
135
|
-
#
|
136
|
-
# @return [String,nil] the ObjectShare version of the String or nil
|
146
|
+
# @param string [String] some string of which we want a Contrast representation.
|
147
|
+
# @return [String, nil] the ObjectShare version of the String or nil
|
137
148
|
def to_cached_string string
|
138
149
|
return Contrast::Utils::ObjectShare::EMPTY_STRING if string.empty?
|
139
150
|
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
|