contrast-agent 4.9.1 → 4.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rspec_parallel +6 -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 -12
- data/ext/cs__contrast_patch/cs__contrast_patch.h +5 -4
- 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 -2
- data/lib/contrast/agent/assess/contrast_object.rb +1 -4
- data/lib/contrast/agent/assess/finalizers/hash.rb +0 -1
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +2 -0
- data/lib/contrast/agent/assess/policy/patcher.rb +0 -1
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -2
- data/lib/contrast/agent/assess/policy/preshift.rb +29 -12
- data/lib/contrast/agent/assess/policy/propagation_method.rb +71 -142
- data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +2 -2
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
- data/lib/contrast/agent/assess/policy/propagator/remove.rb +4 -9
- data/lib/contrast/agent/assess/policy/propagator/split.rb +3 -2
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +1 -0
- data/lib/contrast/agent/assess/policy/rewriter_patch.rb +0 -1
- data/lib/contrast/agent/assess/policy/source_method.rb +15 -88
- data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -1
- data/lib/contrast/agent/assess/policy/trigger_method.rb +45 -172
- data/lib/contrast/agent/assess/policy/trigger_node.rb +52 -19
- data/lib/contrast/agent/assess/property/evented.rb +2 -1
- data/lib/contrast/agent/assess/property/tagged.rb +15 -132
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +0 -1
- data/lib/contrast/agent/deadzone/policy/policy.rb +6 -0
- data/lib/contrast/agent/disable_reaction.rb +1 -1
- data/lib/contrast/agent/exclusion_matcher.rb +0 -4
- data/lib/contrast/agent/inventory/database_config.rb +117 -0
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +7 -5
- data/lib/contrast/agent/inventory/policy/datastores.rb +2 -2
- data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
- data/lib/contrast/agent/middleware.rb +23 -0
- data/lib/contrast/agent/patching/policy/after_load_patch.rb +3 -0
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +17 -12
- data/lib/contrast/agent/patching/policy/method_policy.rb +54 -9
- data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
- data/lib/contrast/agent/patching/policy/patch.rb +42 -238
- data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
- data/lib/contrast/agent/patching/policy/patcher.rb +10 -49
- data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
- data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
- data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
- data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
- data/lib/contrast/agent/reaction_processor.rb +1 -1
- data/lib/contrast/agent/request.rb +9 -4
- data/lib/contrast/agent/request_context.rb +51 -33
- 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 +5 -3
- 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 +16 -3
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +6 -1
- data/lib/contrast/api/communication/messaging_queue.rb +12 -6
- data/lib/contrast/api/communication/service_lifecycle.rb +4 -1
- data/lib/contrast/api/communication/socket_client.rb +4 -4
- data/lib/contrast/api/decorators/agent_startup.rb +4 -4
- data/lib/contrast/api/decorators/application_startup.rb +6 -5
- data/lib/contrast/api/decorators/route_coverage.rb +24 -1
- data/lib/contrast/components/agent.rb +5 -2
- data/lib/contrast/components/api.rb +34 -0
- data/lib/contrast/components/app_context.rb +24 -0
- data/lib/contrast/components/assess.rb +13 -3
- data/lib/contrast/components/base.rb +2 -2
- data/lib/contrast/components/config.rb +91 -11
- data/lib/contrast/components/contrast_service.rb +10 -2
- data/lib/contrast/components/logger.rb +13 -8
- data/lib/contrast/components/scope.rb +9 -28
- data/lib/contrast/config/api_configuration.rb +22 -0
- data/lib/contrast/config/assess_configuration.rb +1 -0
- data/lib/contrast/config/base_configuration.rb +14 -6
- 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 +22 -15
- data/lib/contrast/extension/assess/array.rb +1 -11
- data/lib/contrast/extension/assess/eval_trigger.rb +0 -20
- data/lib/contrast/extension/assess/fiber.rb +0 -11
- data/lib/contrast/extension/assess/hash.rb +0 -10
- data/lib/contrast/extension/assess/kernel.rb +1 -10
- data/lib/contrast/extension/assess/marshal.rb +3 -11
- data/lib/contrast/extension/assess/regexp.rb +0 -11
- data/lib/contrast/extension/assess/string.rb +1 -26
- data/lib/contrast/extension/extension.rb +61 -0
- data/lib/contrast/framework/grape/support.rb +174 -0
- data/lib/contrast/framework/manager.rb +56 -18
- data/lib/contrast/framework/rack/support.rb +1 -1
- data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
- data/lib/contrast/framework/rails/patch/assess_configuration.rb +0 -1
- data/lib/contrast/framework/rails/patch/support.rb +35 -30
- data/lib/contrast/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/active_record_named.rb +1 -0
- data/lib/contrast/framework/rails/support.rb +60 -13
- data/lib/contrast/framework/sinatra/support.rb +1 -1
- data/lib/contrast/logger/application.rb +4 -0
- data/lib/contrast/logger/log.rb +89 -15
- 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 +43 -35
- 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 +16 -13
- data/lib/contrast/utils/tag_util.rb +2 -1
- data/lib/contrast/utils/telemetry.rb +78 -0
- data/lib/contrast/utils/telemetry_identifier.rb +137 -0
- data/lib/contrast.rb +19 -1
- data/resources/assess/policy.json +208 -7
- data/resources/deadzone/policy.json +91 -0
- data/ruby-agent.gemspec +12 -2
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +102 -18
- 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 -39
- data/lib/contrast/utils/inventory_util.rb +0 -113
@@ -13,6 +13,8 @@ module Contrast
|
|
13
13
|
class Support
|
14
14
|
extend Contrast::Framework::BaseSupport
|
15
15
|
extend Contrast::Framework::Rails::Patch::Support
|
16
|
+
include Contrast::Components::Logger::InstanceMethods
|
17
|
+
extend Contrast::Components::Logger::InstanceMethods
|
16
18
|
|
17
19
|
class << self
|
18
20
|
RAILS_MODULE_NAME_VERSION = Gem::Version.new('6.0.0')
|
@@ -49,31 +51,36 @@ module Contrast
|
|
49
51
|
# Find the current route, based on the provided Request wrapper
|
50
52
|
#
|
51
53
|
# @param request[Contrast::Agent::Request]
|
52
|
-
# @return [Contrast::Api::Dtm::RouteCoverage]
|
54
|
+
# @return [Contrast::Api::Dtm::RouteCoverage, nil]
|
53
55
|
def current_route request
|
54
56
|
return unless ::Rails.cs__respond_to?(:application)
|
55
57
|
|
58
|
+
# ActionDispatch::Journey::Path::Pattern::MatchData, Hash, ActionDispatch::Journey::Route, Array<String>
|
56
59
|
match, _params, route, path = get_full_route(request.rack_request)
|
60
|
+
unless route
|
61
|
+
logger.warn('Unable to determine the current route of this request')
|
62
|
+
return
|
63
|
+
end
|
57
64
|
|
58
65
|
original_url = request.rack_request.path_info
|
59
|
-
|
66
|
+
mounted_app = route&.app&.app
|
60
67
|
# Route is either the final rails route, or a router that points to a Sinatra controller.
|
61
|
-
if Contrast::Framework::Sinatra::Support.sinatra_controller?(
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
return Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url)
|
68
|
+
if mounted_app && Contrast::Framework::Sinatra::Support.sinatra_controller?(mounted_app)
|
69
|
+
return mounted_sinatra_route(request, match, path, route, original_url)
|
70
|
+
end
|
71
|
+
if mounted_app && Contrast::Framework::Grape::Support.grape_controller?(mounted_app)
|
72
|
+
return mounted_grape_route(request, match, path, route, original_url)
|
67
73
|
end
|
68
74
|
|
69
75
|
Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route, original_url)
|
70
|
-
rescue StandardError =>
|
76
|
+
rescue StandardError => e
|
77
|
+
logger.warn('Unable to determine the current route of this request', e)
|
71
78
|
nil
|
72
79
|
end
|
73
80
|
|
74
81
|
# Copy a request for modification.
|
75
82
|
#
|
76
|
-
# @param [::ActionDispatch::Request] original env.
|
83
|
+
# @param env [::ActionDispatch::Request] original env.
|
77
84
|
# @return [::ActionDispatch::Request] a copy of original env with rails env merged.
|
78
85
|
def retrieve_request env
|
79
86
|
rails_env = ::Rails.application.env_config.merge(env)
|
@@ -91,18 +98,21 @@ module Contrast
|
|
91
98
|
|
92
99
|
# Determine if route is a Rails engine route.
|
93
100
|
#
|
94
|
-
# @param [Object] app or route that points to a ::Rails::Engine
|
101
|
+
# @param route [Object] app or route that points to a ::Rails::Engine
|
95
102
|
# @return [bool] whether the router is an engine or not.
|
96
103
|
def engine_route? route
|
104
|
+
return false unless route&.app&.app
|
105
|
+
|
97
106
|
route.app.is_a?(::ActionDispatch::Routing::Mapper::Constraints) && route.app.app < ::Rails::Engine
|
98
107
|
end
|
99
108
|
|
100
109
|
# Recursively get final route traversing engines as required.
|
101
110
|
#
|
102
111
|
# @param request [::Rack::Request] the rack request as will be handed to rails controller.
|
103
|
-
# @param top_router [::ActionDispatch::
|
112
|
+
# @param top_router [::ActionDispatch::Journey::Router] the current router relative to the previous.
|
104
113
|
# @param path [Array<String>] the chunks of path that have been seen.
|
105
|
-
# @return [Array<
|
114
|
+
# @return [Array<Object>] the final set of rails route classes.
|
115
|
+
# ActionDispatch::Journey::Path::Pattern::MatchData, Hash, ActionDispatch::Journey::Route, Array<String>
|
106
116
|
def get_full_route request, top_router = ::Rails.application.routes.router, path = []
|
107
117
|
return if (route_matches = top_router.send(:find_routes, request)).empty?
|
108
118
|
|
@@ -132,6 +142,43 @@ module Contrast
|
|
132
142
|
end
|
133
143
|
route_list
|
134
144
|
end
|
145
|
+
|
146
|
+
# @param request[Contrast::Agent::Request]
|
147
|
+
# @param match [ActionDispatch::Journey::Path::Pattern::MatchData]
|
148
|
+
# @param path [Array<String>] the path of this request, built out from each nested
|
149
|
+
# ActionDispatch::Journey::Path::Pattern::MatchData
|
150
|
+
# @param route [::ActionDispatch::Journey::Route]
|
151
|
+
# @param original_url [String] the full url of this request, including the mount
|
152
|
+
# @return [Contrast::Api::Dtm::RouteCoverage, nil]
|
153
|
+
def mounted_sinatra_route request, match, path, route, original_url
|
154
|
+
new_req = unmounted_route(request, match, path)
|
155
|
+
Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url)
|
156
|
+
end
|
157
|
+
|
158
|
+
# @param request[Contrast::Agent::Request]
|
159
|
+
# @param match [ActionDispatch::Journey::Path::Pattern::MatchData]
|
160
|
+
# @param path [Array<String>] the path of this request, built out from each nested
|
161
|
+
# ActionDispatch::Journey::Path::Pattern::MatchData
|
162
|
+
# @param route [::ActionDispatch::Journey::Route]
|
163
|
+
# @param original_url [String] the full url of this request, including the mount
|
164
|
+
# @return [Contrast::Api::Dtm::RouteCoverage, nil]
|
165
|
+
def mounted_grape_route request, match, path, route, original_url
|
166
|
+
new_req = unmounted_route(request, match, path)
|
167
|
+
Contrast::Framework::Grape::Support.current_route(new_req, route.app.app, original_url)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Create a request copied from current request, but with the base path removed from path_info, as that's
|
171
|
+
# the mount.
|
172
|
+
#
|
173
|
+
# @param request[Contrast::Agent::Request]
|
174
|
+
# @param match []
|
175
|
+
# @param path [String] the path of this request
|
176
|
+
# @return [::ActionDispatch::Request]
|
177
|
+
def unmounted_route request, match, path
|
178
|
+
new_req = ::ActionDispatch::Request.new(request.env)
|
179
|
+
new_req.path_info = new_req.path_info.gsub((path << match).join, '')
|
180
|
+
new_req
|
181
|
+
end
|
135
182
|
end
|
136
183
|
end
|
137
184
|
end
|
@@ -106,7 +106,7 @@ module Contrast
|
|
106
106
|
def _route_recurse controller, method, route
|
107
107
|
return if controller.nil? || controller.cs__class == NilClass
|
108
108
|
|
109
|
-
route_patterns = controller.routes.fetch(method
|
109
|
+
route_patterns = controller.routes.fetch(method) { [] }.map(&:first)
|
110
110
|
route_pattern = route_patterns&.find do |matcher|
|
111
111
|
matcher.params(route) # ::Mustermann::Sinatra match.
|
112
112
|
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) }
|
data/lib/contrast/logger/log.rb
CHANGED
@@ -13,6 +13,75 @@ require 'contrast/logger/time'
|
|
13
13
|
require 'contrast/components/config'
|
14
14
|
|
15
15
|
module Contrast
|
16
|
+
# This module allows us to dynamically weave timing into our code, so that only when the time is actually needed do
|
17
|
+
# we pay the penalty for that timing block
|
18
|
+
module TraceTiming
|
19
|
+
def methods_to_time
|
20
|
+
@_methods_to_time ||= []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Store info about methods for later patching.
|
24
|
+
METHOD_INFO = Struct.new(:clazz, :method_name, :custom_msg, :aliased)
|
25
|
+
|
26
|
+
# Add a method to the list of methods to be trace timed if logger set to TRACE. Enables trace timing after if
|
27
|
+
# logger set to TRACE.
|
28
|
+
#
|
29
|
+
# @params: clazz [Class] the class of the method to time.
|
30
|
+
# @params: method [Symbol] the method to time.
|
31
|
+
# @params: method [String] optional custom logging message.
|
32
|
+
def add_method_to_trace_timing clazz, method, msg = nil
|
33
|
+
methods_to_time.append(METHOD_INFO.new(clazz, method, msg, false))
|
34
|
+
enable_trace_timing if logger.level == ::Ougai::Logging::TRACE
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add a method to the list of methods to be trace timed if logger set to TRACE. Enables trace timing after if
|
38
|
+
# logger set to TRACE.
|
39
|
+
#
|
40
|
+
# @params: method_spec [METHOD_INFO] specs about the method to be timed.
|
41
|
+
# @params: class_method [Boolean] whether this is or isn't a class/module method.
|
42
|
+
def trace_time_class_method meth_spec, class_method
|
43
|
+
untimed_func_symbol = "untimed_#{ meth_spec.method_name }".to_sym
|
44
|
+
send_to = class_method ? meth_spec.clazz.cs__singleton_class : meth_spec.clazz
|
45
|
+
meth_spec.clazz.class_eval do
|
46
|
+
include Contrast::Components::Logger::InstanceMethods
|
47
|
+
extend Contrast::Components::Logger::InstanceMethods
|
48
|
+
|
49
|
+
send_to.send(:alias_method, untimed_func_symbol, meth_spec.method_name)
|
50
|
+
meth_spec.aliased = true
|
51
|
+
|
52
|
+
log_message = "Elapsed time for #{ meth_spec.method_name }."
|
53
|
+
log_message = meth_spec.custom_message if meth_spec.custom_msg
|
54
|
+
|
55
|
+
send_to.send(:define_method, meth_spec.method_name) do |*args, **kwargs, &block| # rubocop:disable Performance/Kernel/DefineMethod
|
56
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
57
|
+
rv = if kwargs.empty?
|
58
|
+
send(untimed_func_symbol, *args, &block)
|
59
|
+
else
|
60
|
+
send(untimed_func_symbol, *args, **kwargs, &block)
|
61
|
+
end
|
62
|
+
delta = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
63
|
+
logger.trace(log_message, elapsed: delta * 1000)
|
64
|
+
rv
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Enable trace timing of methods specified in @_methods_to_time via aliasing.
|
70
|
+
def enable_trace_timing
|
71
|
+
methods_to_time.each do |meth_spec|
|
72
|
+
next if meth_spec.aliased
|
73
|
+
|
74
|
+
is_class_method = meth_spec.clazz.singleton_methods(false).include?(meth_spec.method_name)
|
75
|
+
trace_time_class_method meth_spec, is_class_method
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module Contrast
|
82
|
+
# Used as a wrapper around our logging. The module option specifically adds in a new method for error that raises the
|
83
|
+
# logged exception, used in testing so that we can see if anything unexpected happens without it being swallowed
|
84
|
+
# while still providing safe options for customers.
|
16
85
|
module Logger
|
17
86
|
# For development set following env var to raise logged exceptions instead of just logging.
|
18
87
|
if ENV['CONTRAST__AGENT__RUBY_MORE_COWBELL']
|
@@ -20,22 +89,22 @@ module Contrast
|
|
20
89
|
alias_method :cs__error, :error
|
21
90
|
alias_method :cs__warn, :warn
|
22
91
|
|
23
|
-
def error
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
raise exc if exc && exc.cs__class < Exception
|
92
|
+
def error *args, **kwargs
|
93
|
+
if kwargs.empty?
|
94
|
+
cs__error(*args)
|
95
|
+
else
|
96
|
+
cs__error(*args, **kwargs)
|
97
|
+
end
|
98
|
+
args.each { |arg| raise arg if arg && arg.cs__class < Exception }
|
31
99
|
end
|
32
100
|
end
|
33
101
|
end
|
34
102
|
|
35
|
-
# This class functions to serve as a wrapper around our logging, as we need
|
36
|
-
#
|
103
|
+
# This class functions to serve as a wrapper around our logging, as we need to be able to dynamically update
|
104
|
+
# level based on updates to TeamServer.
|
37
105
|
class Log
|
38
106
|
include Singleton
|
107
|
+
include ::Contrast::TraceTiming
|
39
108
|
|
40
109
|
DEFAULT_NAME = 'contrast.log'
|
41
110
|
DEFAULT_LEVEL = ::Ougai::Logging::Severity::INFO
|
@@ -49,8 +118,8 @@ module Contrast
|
|
49
118
|
update
|
50
119
|
end
|
51
120
|
|
52
|
-
# Given new settings from TeamServer, update our logging to use the new
|
53
|
-
#
|
121
|
+
# Given new settings from TeamServer, update our logging to use the new file and level, assuming they weren't
|
122
|
+
# set by local configuration.
|
54
123
|
#
|
55
124
|
# @param log_file [String] the file to which to log, as provided by TeamServer settings
|
56
125
|
# @param log_level [String] the level at which to log, as provided by TeamServer settings
|
@@ -67,6 +136,8 @@ module Contrast
|
|
67
136
|
@previous_path = current_path
|
68
137
|
@previous_level = current_level_const
|
69
138
|
|
139
|
+
enable_trace_timing if current_level_const == ::Ougai::Logging::TRACE
|
140
|
+
|
70
141
|
@_logger = build(path: current_path, level_const: current_level_const)
|
71
142
|
# If we're logging to a new path, then let's start it w/ our helpful
|
72
143
|
# data gathering messages
|
@@ -76,6 +147,9 @@ module Contrast
|
|
76
147
|
logger.error('Unable to process update to LoggerManager.', e)
|
77
148
|
else
|
78
149
|
puts 'Unable to process update to LoggerManager.'
|
150
|
+
raise e if ENV['CONTRAST__AGENT__RUBY_MORE_COWBELL']
|
151
|
+
|
152
|
+
puts e.message
|
79
153
|
puts e.backtrace.join("\n")
|
80
154
|
end
|
81
155
|
end
|
@@ -158,7 +232,8 @@ module Contrast
|
|
158
232
|
# @return [::Ougai::Logging::Severity] the level at which to log
|
159
233
|
def find_valid_level log_level
|
160
234
|
config = ::Contrast::CONFIG.root.agent.logger
|
161
|
-
config_level = config
|
235
|
+
config_level = config&.level&.length&.positive? ? config.level : nil
|
236
|
+
|
162
237
|
valid_level(config_level || log_level)
|
163
238
|
end
|
164
239
|
|
@@ -174,8 +249,7 @@ module Contrast
|
|
174
249
|
DEFAULT_LEVEL
|
175
250
|
end
|
176
251
|
|
177
|
-
# Log that the Agent log has changed and include some default information
|
178
|
-
# at the start of the log.
|
252
|
+
# Log that the Agent log has changed and include some default information at the start of the log.
|
179
253
|
def log_update
|
180
254
|
logger.debug('Initialized new contrast agent logger')
|
181
255
|
logger.debug_with_time('middleware: log environment') do
|
@@ -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
|