contrast-agent 4.12.0 → 4.13.0
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 +5 -0
- data/ext/cs__common/cs__common.h +8 -0
- data/ext/cs__contrast_patch/cs__contrast_patch.c +16 -1
- 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__os_information/extconf.rb +5 -0
- 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/source_method.rb +2 -71
- data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -106
- data/lib/contrast/agent/assess/property/tagged.rb +2 -128
- data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
- 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 +22 -0
- data/lib/contrast/agent/patching/policy/patch.rb +28 -235
- data/lib/contrast/agent/patching/policy/patcher.rb +2 -41
- data/lib/contrast/agent/request_handler.rb +7 -3
- 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/version.rb +1 -1
- data/lib/contrast/agent.rb +6 -0
- data/lib/contrast/components/api.rb +34 -0
- data/lib/contrast/components/app_context.rb +24 -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/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/exclude_key.rb +20 -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/telemetry.rb +78 -0
- data/lib/contrast/utils/telemetry_identifier.rb +137 -0
- data/lib/contrast.rb +18 -0
- 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 -10
@@ -7,6 +7,7 @@ require 'fileutils'
|
|
7
7
|
require 'contrast/config'
|
8
8
|
require 'contrast/utils/object_share'
|
9
9
|
require 'contrast/components/scope'
|
10
|
+
require 'contrast/utils/exclude_key'
|
10
11
|
|
11
12
|
module Contrast
|
12
13
|
# This is how we read in the local settings for the Agent, both ENV/ CMD line
|
@@ -218,6 +219,8 @@ module Contrast
|
|
218
219
|
case convert
|
219
220
|
when Contrast::Config::BaseConfiguration
|
220
221
|
convert.cs__class::KEYS.each_key do |key|
|
222
|
+
next if Contrast::Utils::ExcludeKey.excludable? key.to_s
|
223
|
+
|
221
224
|
hash[key] = convert_to_hash(convert.send(key), {})
|
222
225
|
end
|
223
226
|
hash
|
@@ -16,9 +16,9 @@ module Contrast
|
|
16
16
|
class Manager
|
17
17
|
include Contrast::Components::Logger::InstanceMethods
|
18
18
|
|
19
|
-
# Order here does matter as the first framework listed will be the first one we pull information from
|
20
|
-
#
|
21
|
-
#
|
19
|
+
# Order here does matter as the first framework listed will be the first one we pull information from Rack will
|
20
|
+
# be a special case that may involve updating some logic to handle only applying Rack if Rails/Sinatra do not
|
21
|
+
# exist
|
22
22
|
SUPPORTED_FRAMEWORKS = [
|
23
23
|
Contrast::Framework::Rails::Support, Contrast::Framework::Sinatra::Support,
|
24
24
|
Contrast::Framework::Grape::Support, Contrast::Framework::Rack::Support
|
@@ -34,9 +34,8 @@ module Contrast
|
|
34
34
|
@_frameworks.compact!
|
35
35
|
end
|
36
36
|
|
37
|
-
# Patches that have to be applied as early as possible to catch calls
|
38
|
-
#
|
39
|
-
# configuration.
|
37
|
+
# Patches that have to be applied as early as possible to catch calls that happen prior to the first Request,
|
38
|
+
# typically those around configuration.
|
40
39
|
def before_load_patches!
|
41
40
|
@_before_load_patches ||= begin
|
42
41
|
SUPPORTED_FRAMEWORKS.each(&:before_load_patches!)
|
@@ -81,8 +80,8 @@ module Contrast
|
|
81
80
|
|
82
81
|
# Build a request from the provided env, based on the framework(s) we're currently supporting.
|
83
82
|
#
|
84
|
-
# @param env [Hash] the various variables stored by this and other Middlewares to know the state and values
|
85
|
-
#
|
83
|
+
# @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
|
84
|
+
# this particular Request
|
86
85
|
# @return [::Rack::Request] either a rack request or subclass thereof.
|
87
86
|
def retrieve_request env
|
88
87
|
# If we're mounted on Rails, use Rails.
|
@@ -99,8 +98,8 @@ module Contrast
|
|
99
98
|
logger.warn('Unable to retrieve_request', e)
|
100
99
|
end
|
101
100
|
|
102
|
-
# @param env [Hash] the various variables stored by this and other Middlewares to know the state
|
103
|
-
#
|
101
|
+
# @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
|
102
|
+
# this particular Request
|
104
103
|
# @return [Boolean] true if at least one framework is streaming the response; false if none are streaming
|
105
104
|
def streaming? env
|
106
105
|
result = false
|
@@ -111,8 +110,8 @@ module Contrast
|
|
111
110
|
result
|
112
111
|
end
|
113
112
|
|
114
|
-
# Iterate through current frameworks and return the current request's route. This will be the first
|
115
|
-
#
|
113
|
+
# Iterate through current frameworks and return the current request's route. This will be the first non-nil
|
114
|
+
# result.
|
116
115
|
#
|
117
116
|
# @param request [Contrast::Agent::Request] the current request.
|
118
117
|
# @return [Contrast::Api::Dtm::RouteCoverage] the current route as a Dtm.
|
@@ -125,6 +124,9 @@ module Contrast
|
|
125
124
|
# have support enabled, we'll enable it now. We'll also need to catch up on any other startup actions that we've
|
126
125
|
# missed. Most likely, this is only necessary for those applications which have applications mounted on them.
|
127
126
|
#
|
127
|
+
# TODO: RUBY-1354
|
128
|
+
# TODO: RUBY-1356
|
129
|
+
#
|
128
130
|
# @param mod [Module] the module or class that was just loaded
|
129
131
|
def register_late_framework mod
|
130
132
|
return unless mod
|
@@ -5,10 +5,14 @@ module Contrast
|
|
5
5
|
module Framework
|
6
6
|
module Rails
|
7
7
|
module Patch
|
8
|
-
# This class acts as our patch into the ActionController::Live::Buffer
|
9
|
-
#
|
8
|
+
# This class acts as our patch into the ActionController::Live::Buffer class, allowing us to track the close
|
9
|
+
# event on streamed responses.
|
10
10
|
module ActionControllerLiveBuffer
|
11
11
|
class << self
|
12
|
+
# TODO: RUBY-1353
|
13
|
+
# TODO: RUBY-1355
|
14
|
+
# TODO: RUBY-1357
|
15
|
+
# TODO: RUBY-1357
|
12
16
|
def send_messages
|
13
17
|
return unless (context = Contrast::Agent::REQUEST_TRACKER.current)
|
14
18
|
|
@@ -20,10 +24,9 @@ module Contrast
|
|
20
24
|
def instrument
|
21
25
|
@_instrument ||= begin
|
22
26
|
::ActionController::Live::Buffer.class_eval do
|
23
|
-
# normally pre->in->post filters are applied however, in a streamed response
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# been written we need to explicitly send them
|
27
|
+
# normally pre->in->post filters are applied however, in a streamed response we can run into a case
|
28
|
+
# where it's pre -> in -> post -> more infilters in order to submit anything found during the
|
29
|
+
# infilters after the response has been written we need to explicitly send them
|
27
30
|
alias_method :cs__close, :close
|
28
31
|
def close
|
29
32
|
Contrast::Framework::Rails::Patch::ActionControllerLiveBuffer.send_messages
|
@@ -38,37 +38,39 @@ module Contrast
|
|
38
38
|
instrumenting_module:
|
39
39
|
'Contrast::Framework::Rails::Patch::RailsApplicationConfiguration')
|
40
40
|
])
|
41
|
-
if RUBY_VERSION < '2.6.0'
|
42
|
-
patches.merge([
|
43
|
-
# TODO: RUBY-714 remove w/ EOL of 2.5
|
44
|
-
#
|
45
|
-
# @deprecated Everything past here is used for Rewriting and can
|
46
|
-
# be removed once we no longer support 2.5.
|
47
|
-
Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
|
48
|
-
'ActionController::Railties::Helper::ClassMethods',
|
49
|
-
'contrast/framework/rails/rewrite/action_controller_railties_helper_inherited',
|
50
|
-
method_to_instrument: :inherited,
|
51
|
-
instrumenting_module:
|
52
|
-
'Contrast::Framework::Rails::Rewrite::ActionControllerRailtiesHelperInherited'),
|
53
|
-
Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
|
54
|
-
'ActiveRecord::AttributeMethods::Read::ClassMethods',
|
55
|
-
'contrast/framework/rails/rewrite/active_record_attribute_methods_read',
|
56
|
-
instrumenting_module:
|
57
|
-
'Contrast::Framework::Rails::Rewrite::ActiveRecordAttributeMethodsRead'),
|
58
|
-
Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
|
59
|
-
'ActiveRecord::Scoping::Named::ClassMethods',
|
60
|
-
'contrast/framework/rails/rewrite/active_record_named',
|
61
|
-
instrumenting_module: 'Contrast::Framework::Rails::Rewrite::ActiveRecordNamed'),
|
62
|
-
Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
|
63
|
-
'ActiveRecord::AttributeMethods::TimeZoneConversion::ClassMethods',
|
64
|
-
'contrast/framework/rails/rewrite/active_record_time_zone_inherited',
|
65
|
-
method_to_instrument: :inherited,
|
66
|
-
instrumenting_module:
|
67
|
-
'Contrast::Framework::Rails::Rewrite::ActiveRecordTimeZoneInherited')
|
68
|
-
])
|
69
|
-
end
|
41
|
+
patches.merge(special_after_load_patches) if RUBY_VERSION < '2.6.0'
|
70
42
|
patches
|
71
43
|
end
|
44
|
+
|
45
|
+
def special_after_load_patches
|
46
|
+
[
|
47
|
+
# TODO: RUBY-714 remove w/ EOL of 2.5
|
48
|
+
#
|
49
|
+
# @deprecated Everything past here is used for Rewriting and can
|
50
|
+
# be removed once we no longer support 2.5.
|
51
|
+
Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
|
52
|
+
'ActionController::Railties::Helper::ClassMethods',
|
53
|
+
'contrast/framework/rails/rewrite/action_controller_railties_helper_inherited',
|
54
|
+
method_to_instrument: :inherited,
|
55
|
+
instrumenting_module:
|
56
|
+
'Contrast::Framework::Rails::Rewrite::ActionControllerRailtiesHelperInherited'),
|
57
|
+
Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
|
58
|
+
'ActiveRecord::AttributeMethods::Read::ClassMethods',
|
59
|
+
'contrast/framework/rails/rewrite/active_record_attribute_methods_read',
|
60
|
+
instrumenting_module:
|
61
|
+
'Contrast::Framework::Rails::Rewrite::ActiveRecordAttributeMethodsRead'),
|
62
|
+
Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
|
63
|
+
'ActiveRecord::Scoping::Named::ClassMethods',
|
64
|
+
'contrast/framework/rails/rewrite/active_record_named',
|
65
|
+
instrumenting_module: 'Contrast::Framework::Rails::Rewrite::ActiveRecordNamed'),
|
66
|
+
Contrast::Agent::Patching::Policy::AfterLoadPatch.new(
|
67
|
+
'ActiveRecord::AttributeMethods::TimeZoneConversion::ClassMethods',
|
68
|
+
'contrast/framework/rails/rewrite/active_record_time_zone_inherited',
|
69
|
+
method_to_instrument: :inherited,
|
70
|
+
instrumenting_module:
|
71
|
+
'Contrast::Framework::Rails::Rewrite::ActiveRecordTimeZoneInherited')
|
72
|
+
]
|
73
|
+
end
|
72
74
|
end
|
73
75
|
end
|
74
76
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require 'contrast/utils/exclude_key'
|
5
|
+
|
4
6
|
module Contrast
|
5
7
|
module Logger
|
6
8
|
# Our decorator for the Ougai logger allowing for the logging of the
|
@@ -29,6 +31,8 @@ module Contrast
|
|
29
31
|
loggable = ::Contrast::CONFIG.loggable
|
30
32
|
info('Current configuration', configuration: loggable)
|
31
33
|
env_keys = ENV.keys.select do |env_key|
|
34
|
+
next if Contrast::Utils::ExcludeKey.excludable? env_key.to_s
|
35
|
+
|
32
36
|
env_key&.to_s&.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER)
|
33
37
|
end
|
34
38
|
env_items = env_keys.map { |env_key| Contrast::Utils::EnvConfigurationItem.new(env_key, nil) }
|
@@ -0,0 +1,129 @@
|
|
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 PropagationMethod module
|
8
|
+
# and some other module methods from the same place, so we can ease the main module
|
9
|
+
module PropagationMethodUtils
|
10
|
+
APPEND_ACTION = 'APPEND'
|
11
|
+
CENTER_ACTION = 'CENTER'
|
12
|
+
INSERT_ACTION = 'INSERT'
|
13
|
+
KEEP_ACTION = 'KEEP'
|
14
|
+
NEXT_ACTION = 'NEXT'
|
15
|
+
NOOP_ACTION = 'NOOP'
|
16
|
+
PREPEND_ACTION = 'PREPEND'
|
17
|
+
REPLACE_ACTION = 'REPLACE'
|
18
|
+
REMOVE_ACTION = 'REMOVE'
|
19
|
+
REVERSE_ACTION = 'REVERSE'
|
20
|
+
SPLAT_ACTION = 'SPLAT'
|
21
|
+
SPLIT_ACTION = 'SPLIT'
|
22
|
+
DB_WRITE_ACTION = 'DB_WRITE'
|
23
|
+
CUSTOM_ACTION = 'CUSTOM'
|
24
|
+
|
25
|
+
ZERO_LENGTH_ACTIONS = [DB_WRITE_ACTION, CUSTOM_ACTION, KEEP_ACTION, REPLACE_ACTION, SPLAT_ACTION].cs__freeze
|
26
|
+
|
27
|
+
PROPAGATION_ACTIONS = {
|
28
|
+
APPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Append,
|
29
|
+
CENTER_ACTION => Contrast::Agent::Assess::Policy::Propagator::Center,
|
30
|
+
INSERT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Insert,
|
31
|
+
KEEP_ACTION => Contrast::Agent::Assess::Policy::Propagator::Keep,
|
32
|
+
NEXT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Next,
|
33
|
+
NOOP_ACTION => nil,
|
34
|
+
PREPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Prepend,
|
35
|
+
REPLACE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Replace,
|
36
|
+
REMOVE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Remove,
|
37
|
+
REVERSE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Reverse,
|
38
|
+
SPLAT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Splat,
|
39
|
+
SPLIT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Split
|
40
|
+
}.cs__freeze
|
41
|
+
|
42
|
+
def determine_target propagation_node, ret, object, args
|
43
|
+
target = propagation_node.targets[0]
|
44
|
+
case target
|
45
|
+
when Contrast::Utils::ObjectShare::OBJECT_KEY
|
46
|
+
object
|
47
|
+
when Contrast::Utils::ObjectShare::RETURN_KEY
|
48
|
+
ret
|
49
|
+
else
|
50
|
+
args[target]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Custom actions tend to be the more complex of our propagations. Often, the method has to make decisions
|
55
|
+
# about the target based on the context with which the method was called. As such, defer determining if the
|
56
|
+
# target is valid to that method.
|
57
|
+
#
|
58
|
+
# In all other cases, a target is valid for propagation if it is not nil
|
59
|
+
#
|
60
|
+
# @param target [Object] the thing to which to propagate
|
61
|
+
# @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
|
62
|
+
# propagation event.
|
63
|
+
# @return [Boolean]
|
64
|
+
def valid_target? target, propagation_node
|
65
|
+
return true if propagation_node.action == CUSTOM_ACTION
|
66
|
+
|
67
|
+
!!target
|
68
|
+
end
|
69
|
+
|
70
|
+
# If the action required needs a length and the target does not have one, the length is not valid
|
71
|
+
#
|
72
|
+
# @param target [Object] the thing to which to propagate
|
73
|
+
# @param action [String] the name of the action taken during this propagation
|
74
|
+
# @return [Boolean]
|
75
|
+
def valid_length? target, action
|
76
|
+
return true if ZERO_LENGTH_ACTIONS.include?(action)
|
77
|
+
|
78
|
+
if Contrast::Utils::DuckUtils.quacks_to?(target, :length)
|
79
|
+
target.length != 0 # rubocop:disable Style/ZeroLengthPredicate
|
80
|
+
else
|
81
|
+
!target.to_s.empty?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Before we do any work, we should check if we even need to. If the source and target of this patcher are
|
86
|
+
# not tracked, there's no need to do anything. A copy of nothing is still nothing.
|
87
|
+
#
|
88
|
+
# @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
|
89
|
+
# propagation event.
|
90
|
+
# @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
|
91
|
+
# the invocation of the patched method.
|
92
|
+
# @param target [Object] the thing to which to propagate
|
93
|
+
# @return [Boolean]
|
94
|
+
def can_propagate? propagation_node, preshift, target
|
95
|
+
return false unless appropriate_target?(propagation_node, target)
|
96
|
+
return true if Contrast::Utils::Assess::TrackingUtil.tracked?(target)
|
97
|
+
return false unless preshift
|
98
|
+
|
99
|
+
propagation_node.sources.each do |source|
|
100
|
+
case source
|
101
|
+
when Contrast::Utils::ObjectShare::OBJECT_KEY
|
102
|
+
return true if Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.object)
|
103
|
+
else
|
104
|
+
# has to be P, there's no ret source type (yet? ever?)
|
105
|
+
return true if preshift.args && Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.args[source])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
# We cannot propagate to frozen things that have not been updated to work with our property tracking,
|
112
|
+
# unless they're duplicable and the return. We probably shouldn't propagate to frozen things at all, as
|
113
|
+
# they're supposed to be immutable, but third parties do jenky things, so allow it as long as it is safe to
|
114
|
+
# do.
|
115
|
+
#
|
116
|
+
# @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
|
117
|
+
# propagation event.
|
118
|
+
# @param target [Object] the Target to which to propagate.
|
119
|
+
# @return [Boolean] if the target can be propagated to
|
120
|
+
def appropriate_target? propagation_node, target
|
121
|
+
# special handle Returns b/c we can do unfreezing magic during propagation
|
122
|
+
return true if propagation_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
|
123
|
+
|
124
|
+
Contrast::Agent::Assess::Tracker.trackable?(target)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -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
|