contrast-agent 4.2.0 → 4.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +1 -0
- data/ext/cs__assess_marshal_module/cs__assess_marshal_module.c +22 -10
- data/ext/cs__assess_marshal_module/cs__assess_marshal_module.h +4 -3
- data/lib/contrast/agent.rb +5 -1
- data/lib/contrast/agent/assess.rb +0 -9
- data/lib/contrast/agent/assess/contrast_event.rb +49 -132
- data/lib/contrast/agent/assess/contrast_object.rb +54 -0
- data/lib/contrast/agent/assess/events/source_event.rb +4 -9
- data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
- data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
- data/lib/contrast/agent/assess/policy/policy_node.rb +31 -59
- data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
- data/lib/contrast/agent/assess/policy/propagation_method.rb +41 -32
- data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
- data/lib/contrast/agent/assess/policy/propagator/append.rb +29 -15
- data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -18
- data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -2
- data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
- data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
- data/lib/contrast/agent/assess/policy/propagator/select.rb +3 -4
- data/lib/contrast/agent/assess/policy/propagator/splat.rb +25 -17
- data/lib/contrast/agent/assess/policy/propagator/split.rb +83 -120
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +41 -25
- data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
- data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
- data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +5 -8
- data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
- data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
- data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
- data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
- data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +2 -3
- data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
- data/lib/contrast/agent/assess/properties.rb +0 -2
- data/lib/contrast/agent/assess/property/tagged.rb +56 -32
- data/lib/contrast/agent/assess/tracker.rb +16 -18
- data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
- data/lib/contrast/agent/middleware.rb +134 -55
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +4 -0
- data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
- data/lib/contrast/agent/patching/policy/patch.rb +4 -4
- data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
- data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
- data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
- data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
- data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
- data/lib/contrast/agent/protect/rule/base.rb +63 -14
- data/lib/contrast/agent/protect/rule/cmd_injection.rb +3 -3
- data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
- data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
- data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
- data/lib/contrast/agent/protect/rule/sqli.rb +20 -14
- data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
- data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
- data/lib/contrast/agent/reaction_processor.rb +1 -1
- data/lib/contrast/agent/request_context.rb +12 -0
- data/lib/contrast/agent/response.rb +5 -5
- data/lib/contrast/agent/rewriter.rb +3 -3
- data/lib/contrast/agent/scope.rb +33 -13
- data/lib/contrast/agent/static_analysis.rb +13 -7
- data/lib/contrast/agent/thread.rb +1 -1
- data/lib/contrast/agent/thread_watcher.rb +20 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api/communication/messaging_queue.rb +18 -21
- data/lib/contrast/api/communication/response_processor.rb +8 -1
- data/lib/contrast/api/communication/socket_client.rb +22 -14
- data/lib/contrast/api/decorators.rb +2 -0
- data/lib/contrast/api/decorators/agent_startup.rb +58 -0
- data/lib/contrast/api/decorators/application_startup.rb +51 -0
- data/lib/contrast/api/decorators/library.rb +1 -0
- data/lib/contrast/api/decorators/library_usage_update.rb +1 -0
- data/lib/contrast/api/decorators/route_coverage.rb +15 -5
- data/lib/contrast/api/decorators/trace_event.rb +58 -42
- data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
- data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
- data/lib/contrast/api/decorators/user_input.rb +2 -1
- data/lib/contrast/common_agent_configuration.rb +1 -1
- data/lib/contrast/components/agent.rb +2 -0
- data/lib/contrast/components/app_context.rb +4 -22
- data/lib/contrast/components/assess.rb +36 -0
- data/lib/contrast/components/interface.rb +5 -3
- data/lib/contrast/components/sampling.rb +48 -6
- data/lib/contrast/components/scope.rb +23 -0
- data/lib/contrast/components/settings.rb +8 -7
- data/lib/contrast/config/assess_configuration.rb +2 -1
- data/lib/contrast/extension/assess/array.rb +1 -2
- data/lib/contrast/extension/assess/erb.rb +1 -3
- data/lib/contrast/extension/assess/exec_trigger.rb +1 -1
- data/lib/contrast/extension/assess/fiber.rb +2 -3
- data/lib/contrast/extension/assess/hash.rb +4 -2
- data/lib/contrast/extension/assess/kernel.rb +1 -2
- data/lib/contrast/extension/assess/marshal.rb +34 -26
- data/lib/contrast/extension/assess/regexp.rb +3 -8
- data/lib/contrast/extension/assess/string.rb +1 -2
- data/lib/contrast/framework/base_support.rb +51 -53
- data/lib/contrast/framework/manager.rb +16 -14
- data/lib/contrast/framework/rack/patch/session_cookie.rb +1 -1
- data/lib/contrast/framework/rack/support.rb +2 -1
- data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -1
- data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +1 -1
- data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +1 -1
- data/lib/contrast/framework/rails/support.rb +44 -44
- data/lib/contrast/framework/sinatra/support.rb +102 -42
- data/lib/contrast/logger/application.rb +0 -3
- data/lib/contrast/logger/log.rb +31 -15
- data/lib/contrast/utils/class_util.rb +3 -1
- data/lib/contrast/utils/duck_utils.rb +1 -1
- data/lib/contrast/utils/heap_dump_util.rb +103 -87
- data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
- data/lib/contrast/utils/object_share.rb +3 -3
- data/lib/contrast/utils/preflight_util.rb +1 -1
- data/lib/contrast/utils/resource_loader.rb +1 -1
- data/lib/contrast/utils/sha256_builder.rb +2 -2
- data/lib/contrast/utils/string_utils.rb +1 -1
- data/lib/contrast/utils/tag_util.rb +9 -13
- data/resources/assess/policy.json +12 -18
- data/resources/deadzone/policy.json +150 -0
- data/resources/protect/policy.json +12 -0
- data/ruby-agent.gemspec +60 -19
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +124 -112
- data/lib/contrast/agent/assess/rule.rb +0 -18
- data/lib/contrast/agent/assess/rule/base.rb +0 -52
- data/lib/contrast/agent/assess/rule/redos.rb +0 -67
- data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
- data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
- data/lib/contrast/utils/prevent_serialization.rb +0 -52
@@ -2,14 +2,13 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'contrast/framework/base_support'
|
5
|
-
require 'contrast/framework/sinatra/patch/support'
|
6
5
|
|
7
6
|
module Contrast
|
8
7
|
module Framework
|
9
8
|
module Sinatra
|
10
9
|
# Used when Sinatra is present to define framework specific behavior
|
11
|
-
class Support
|
12
|
-
extend Contrast::Framework::
|
10
|
+
class Support
|
11
|
+
extend Contrast::Framework::BaseSupport
|
13
12
|
class << self
|
14
13
|
def detection_class
|
15
14
|
'Sinatra'
|
@@ -20,44 +19,74 @@ module Contrast
|
|
20
19
|
end
|
21
20
|
|
22
21
|
def application_name
|
23
|
-
|
24
|
-
|
25
|
-
app_class.cs__class.cs__name
|
22
|
+
app_class&.cs__name
|
26
23
|
end
|
27
24
|
|
28
25
|
def application_root
|
29
|
-
|
30
|
-
|
31
|
-
app_class.root
|
26
|
+
app_instance&.root
|
32
27
|
end
|
33
28
|
|
34
29
|
def server_type
|
35
30
|
'sinatra'
|
36
31
|
end
|
37
32
|
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
33
|
+
# Given an object, determine if it is a Sinatra controller with routes.
|
34
|
+
#
|
35
|
+
# @param app [Object] suspected Sinatra app.
|
36
|
+
# @return [Boolean]
|
37
|
+
def sinatra_controller? app
|
38
|
+
# Sinatra is loaded?
|
39
|
+
return false unless defined?(::Sinatra) && defined?(::Sinatra::Base)
|
40
|
+
# App is a subclass of or actually is ::Sinatra::Base.
|
41
|
+
return false unless (app.cs__respond_to?(:<) && app < ::Sinatra::Base) || app == ::Sinatra::Base
|
42
|
+
|
43
|
+
# App has routes.
|
44
|
+
!app.routes.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Find all classes that subclass ::Sinatra::Base. Gather their routes.
|
48
|
+
#
|
49
|
+
# @return [Array<Contrast::Api::Dtm::RouteCoverage>] the routes found as Dtms.
|
41
50
|
def collect_routes
|
51
|
+
return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless defined?(::Sinatra) && defined?(::Sinatra::Base)
|
52
|
+
|
42
53
|
routes = []
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
class_routes.each_pair do |method, list|
|
49
|
-
# item: [ Mustermann::Sinatra, [], Proc]
|
50
|
-
list.each do |item|
|
51
|
-
routes << Contrast::Api::Dtm::RouteCoverage.from_sinatra_route(clazz, method, item[0])
|
54
|
+
sinatra_controllers.each do |controller|
|
55
|
+
controller.routes.each_pair do |method, route_triplets|
|
56
|
+
# Sinatra stores its routes as a triplet: [Mustermann::Sinatra, [], Proc]
|
57
|
+
route_triplets.map(&:first).each do |route_pattern|
|
58
|
+
routes << Contrast::Api::Dtm::RouteCoverage.from_sinatra_route(controller, method, route_pattern)
|
52
59
|
end
|
53
60
|
end
|
54
61
|
end
|
55
62
|
routes
|
56
63
|
end
|
57
64
|
|
58
|
-
#
|
59
|
-
|
60
|
-
|
65
|
+
# Given the current request return a RouteCoverage dtm.
|
66
|
+
#
|
67
|
+
# @param request [Contrast::Agent::Request] a contrast tracked request.
|
68
|
+
# @param controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base.
|
69
|
+
# @return [Contrast::Api::Dtm::RouteCoverage, nil] a Dtm describing the route
|
70
|
+
# matched to the request if a match was found.
|
71
|
+
def current_route request, controller = ::Sinatra::Base, full_route = nil
|
72
|
+
return unless sinatra_controller?(controller)
|
73
|
+
|
74
|
+
method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc...
|
75
|
+
|
76
|
+
# Find route match--checking superclasses if necessary.
|
77
|
+
final_controller, route_pattern = _route_recurse(controller, method, _cleaned_route(request))
|
78
|
+
return unless !final_controller.nil? && !route_pattern.nil?
|
79
|
+
|
80
|
+
full_route ||= request.path_info
|
81
|
+
|
82
|
+
Contrast::Api::Dtm::RouteCoverage.from_sinatra_route(final_controller, method, route_pattern, full_route)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Search object space for sinatra controllers--any class that subclasses ::Sinatra::Base.
|
86
|
+
#
|
87
|
+
# @return [Array<::Sinatra::Base>] sinatra controlelrs
|
88
|
+
def sinatra_controllers
|
89
|
+
[::Sinatra::Base] + ObjectSpace.each_object(Class).select { |clazz| sinatra_controller?(clazz) }
|
61
90
|
end
|
62
91
|
|
63
92
|
def retrieve_request env
|
@@ -66,30 +95,61 @@ module Contrast
|
|
66
95
|
|
67
96
|
private
|
68
97
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
98
|
+
# Given a controller and a route to match against, find the route_pattern and class that will serve the
|
99
|
+
# route. This is recursive as Sinatra's routing is recursive from subclass to super.
|
100
|
+
#
|
101
|
+
# @param controller [Sinatra::Base, #routes] a Sinatra application.
|
102
|
+
# @param method [::Rack::REQUEST_METHOD] GET, POST, PUT, etc...
|
103
|
+
# @param method [String] the relative route passed from Rack.
|
104
|
+
# @return [Array[Sinatra::Base, Mustermann::Sinatra], nil] Either the controller that
|
105
|
+
# will handle the route along with the route pattern or nil if no match.
|
106
|
+
def _route_recurse controller, method, route
|
107
|
+
return if controller.nil? || controller.cs__class == NilClass
|
108
|
+
|
109
|
+
route_patterns = controller.routes.fetch(method, []).map(&:first)
|
110
|
+
route_pattern = route_patterns&.find do |matcher|
|
111
|
+
matcher.params(route) # ::Mustermann::Sinatra match.
|
76
112
|
end
|
113
|
+
|
114
|
+
return controller, route_pattern if route_pattern
|
115
|
+
|
116
|
+
# Check routes defined in superclass if present.
|
117
|
+
return _route_recurse(controller.superclass, method, route) if controller.superclass&.instance_variable_get(:@routes)
|
77
118
|
end
|
78
119
|
|
79
|
-
#
|
80
|
-
#
|
81
|
-
# Contrast::
|
82
|
-
|
83
|
-
|
120
|
+
# Get route and do some cleanup matching that of Sinatra::Base#process_route.
|
121
|
+
#
|
122
|
+
# @param request [Contrast::Agent::Request] a contrast tracked request.
|
123
|
+
# @return [String] the extracted and cleaned relative route.
|
124
|
+
def _cleaned_route request
|
125
|
+
settings = ::Sinatra::Base.settings
|
126
|
+
route = request.env[::Rack::PATH_INFO]
|
127
|
+
return '/' if route.empty? && !settings.empty_path_info?
|
128
|
+
|
129
|
+
!settings.strict_paths? && route.end_with?('/') ? route[0..-2] : route
|
130
|
+
end
|
131
|
+
|
132
|
+
# Almost an alias to app_instance.
|
133
|
+
#
|
134
|
+
# @return [::Sinatra::Base] the current controller class as routed by Rack.
|
135
|
+
def app_class
|
136
|
+
return unless defined?(::Sinatra) && defined?(::Sinatra::Base)
|
84
137
|
|
85
|
-
|
138
|
+
app_instance.cs__class
|
86
139
|
end
|
87
140
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
141
|
+
# Search the object space for the controller handling this request which will be
|
142
|
+
# the class inheriting from ::Sinatra::Base with @app=nil since it is the final servicer
|
143
|
+
# in the request/middleware chain.
|
144
|
+
#
|
145
|
+
# @return [::Sinatra::Base] the current controller as routed by Rack.
|
146
|
+
def app_instance
|
147
|
+
return unless defined?(::Sinatra) && defined?(::Sinatra::Base)
|
148
|
+
|
149
|
+
@_app_instance ||= begin
|
150
|
+
sinatra_layers = ObjectSpace.each_object(::Sinatra::Base).to_a
|
151
|
+
sinatra_layers.find { |layer| layer.app.nil? }
|
152
|
+
end
|
93
153
|
end
|
94
154
|
end
|
95
155
|
end
|
data/lib/contrast/logger/log.rb
CHANGED
@@ -37,28 +37,22 @@ module Contrast
|
|
37
37
|
# Given new settings from TeamServer, update our logging to use the new
|
38
38
|
# file and level, assuming they weren't set by local configuration.
|
39
39
|
#
|
40
|
-
# @param log_file [String] the file to which to log
|
41
|
-
# @param log_level [String] the level at which to log
|
40
|
+
# @param log_file [String] the file to which to log, as provided by TeamServer settings
|
41
|
+
# @param log_level [String] the level at which to log, as provided by TeamServer settings
|
42
42
|
def update log_file = nil, log_level = nil
|
43
|
-
|
44
|
-
|
45
|
-
config_path = config.path&.length.to_i.positive? ? config.path : nil
|
46
|
-
config_level = config.level&.length&.positive? ? config.level : nil
|
47
|
-
|
48
|
-
# config > settings > default
|
49
|
-
path = valid_path(config_path || log_file)
|
50
|
-
level_const = valid_level(config_level || log_level)
|
43
|
+
current_path = find_valid_path(log_file)
|
44
|
+
current_level_const = find_valid_level(log_level)
|
51
45
|
|
52
|
-
path_change =
|
53
|
-
level_change =
|
46
|
+
path_change = current_path != previous_path
|
47
|
+
level_change = current_level_const != previous_level
|
54
48
|
|
55
49
|
# don't needlessly recreate logger
|
56
50
|
return if @_logger && !(path_change || level_change)
|
57
51
|
|
58
|
-
@previous_path =
|
59
|
-
@previous_level =
|
52
|
+
@previous_path = current_path
|
53
|
+
@previous_level = current_level_const
|
60
54
|
|
61
|
-
@_logger = build(path:
|
55
|
+
@_logger = build(path: current_path, level_const: current_level_const)
|
62
56
|
# If we're logging to a new path, then let's start it w/ our helpful
|
63
57
|
# data gathering messages
|
64
58
|
log_update if path_change
|
@@ -108,6 +102,17 @@ module Contrast
|
|
108
102
|
logger.extend(Contrast::Logger::Time)
|
109
103
|
end
|
110
104
|
|
105
|
+
# Determine the valid path to which to log, given the precedence of config > settings > default.
|
106
|
+
#
|
107
|
+
# @param log_file [String, nil] the file to which to log as provided by the settings retrieved from the
|
108
|
+
# TeamServer.
|
109
|
+
# @return [String] the path to which to log or STDOUT / STDERR if one of those values provided.
|
110
|
+
def find_valid_path log_file
|
111
|
+
config = CONFIG.root.agent.logger
|
112
|
+
config_path = config.path&.length.to_i.positive? ? config.path : nil
|
113
|
+
valid_path(config_path || log_file)
|
114
|
+
end
|
115
|
+
|
111
116
|
def valid_path path
|
112
117
|
path = path.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : path
|
113
118
|
return path if path == STDOUT_STR
|
@@ -129,6 +134,17 @@ module Contrast
|
|
129
134
|
end
|
130
135
|
end
|
131
136
|
|
137
|
+
# Determine the valid level to which to log, given the precedence of config > settings > default.
|
138
|
+
#
|
139
|
+
# @param log_level [String, nil] the level at which to log as provided by the settings retrieved from the
|
140
|
+
# TeamServer.
|
141
|
+
# @return [::Ougai::Logging::Severity] the level at which to log
|
142
|
+
def find_valid_level log_level
|
143
|
+
config = CONFIG.root.agent.logger
|
144
|
+
config_level = config.level&.length&.positive? ? config.level : nil
|
145
|
+
valid_level(config_level || log_level)
|
146
|
+
end
|
147
|
+
|
132
148
|
def valid_level level
|
133
149
|
level ||= DEFAULT_LEVEL
|
134
150
|
level = level.upcase
|
@@ -52,7 +52,7 @@ module Contrast
|
|
52
52
|
# Return a String representing the object invoking this method in the
|
53
53
|
# form expected by our dataflow events.
|
54
54
|
#
|
55
|
-
# @param object [Object] the entity to convert to a String
|
55
|
+
# @param object [Object, nil] the entity to convert to a String
|
56
56
|
# @return [String] the human readable form of the String, as defined by
|
57
57
|
# https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md
|
58
58
|
def to_contrast_string object
|
@@ -63,6 +63,8 @@ module Contrast
|
|
63
63
|
return cached if cached
|
64
64
|
|
65
65
|
object.dup
|
66
|
+
elsif object.nil?
|
67
|
+
Contrast::Utils::ObjectShare::NIL_STRING
|
66
68
|
elsif object.cs__is_a?(Symbol)
|
67
69
|
":#{ object }"
|
68
70
|
elsif object.cs__is_a?(Numeric)
|
@@ -4,7 +4,7 @@
|
|
4
4
|
module Contrast
|
5
5
|
module Utils
|
6
6
|
# Utility methods for identifying instances that can be used interchangeably
|
7
|
-
|
7
|
+
module DuckUtils
|
8
8
|
class << self
|
9
9
|
# Determine if the given object, or the object to which it delegates,
|
10
10
|
# responds to the given method.
|
@@ -2,12 +2,13 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'objspace'
|
5
|
+
require 'singleton'
|
5
6
|
require 'contrast/components/interface'
|
6
7
|
|
7
8
|
module Contrast
|
8
9
|
module Utils
|
9
10
|
# Implementation of a heap dump util to automate generation
|
10
|
-
class HeapDumpUtil
|
11
|
+
class HeapDumpUtil < Contrast::Agent::WorkerThread
|
11
12
|
include Contrast::Components::Interface
|
12
13
|
access_component :heap_dump, :logging
|
13
14
|
|
@@ -15,98 +16,113 @@ module Contrast
|
|
15
16
|
FILE_WRITE_FLAGS = 'w'
|
16
17
|
|
17
18
|
class << self
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
return unless File.writable?(dir)
|
25
|
-
|
26
|
-
delay = heap_dump_control[:delay]
|
27
|
-
Contrast::Agent::Thread.new do
|
28
|
-
logger.info("HEAP DUMP THREAD INITIALIZED. WAITING #{ delay } SECONDS TO BEGIN.")
|
29
|
-
sleep(delay)
|
30
|
-
capture_heap_dump
|
31
|
-
end
|
32
|
-
rescue StandardError => e
|
33
|
-
logger.info(LOG_ERROR_DUMPS, e)
|
34
|
-
nil
|
19
|
+
def enabled?
|
20
|
+
heap_dump_enabled?
|
21
|
+
end
|
22
|
+
|
23
|
+
def control
|
24
|
+
heap_dump_control
|
35
25
|
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_thread!
|
29
|
+
return unless Contrast::Utils::HeapDumpUtil.enabled?
|
36
30
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
*****************************************************
|
49
|
-
|
50
|
-
Heap dump is a debugging tool that snapshots the entire
|
51
|
-
state of the Ruby VM. It is an exceptionally expensive
|
52
|
-
process, and should only be used to debug especially
|
53
|
-
pernicious errors.
|
54
|
-
|
55
|
-
It will write multiple memory snaphots, which are liable
|
56
|
-
to be multiple gigabytes in size.
|
57
|
-
They will be named "[unix timestamp]-heap.dump",
|
58
|
-
e.g.: 1020304050-heap.dump
|
59
|
-
|
60
|
-
It will then call Ruby `exit()`.
|
61
|
-
|
62
|
-
If this is not your specific intent, you can (and should)
|
63
|
-
disable this option in your Contrast config file.
|
64
|
-
|
65
|
-
HEAP DUMP PARAMETERS:
|
66
|
-
\t[write files to this directory] dir: #{ dir }
|
67
|
-
\t[wait this many seconds in between dumps] window: #{ window }
|
68
|
-
\t[heap dump this many times] count: #{ count }
|
69
|
-
\t[wait this many seconds into app lifetime] delay: #{ delay }
|
70
|
-
\t[perform gc pass before dump] clean: #{ clean }
|
71
|
-
|
72
|
-
*****************************************************
|
73
|
-
******** YOU HAVE BEEN WARNED ********
|
74
|
-
*****************************************************
|
75
|
-
WARNING
|
31
|
+
control = Contrast::Utils::HeapDumpUtil.control
|
32
|
+
log_enabled_warning
|
33
|
+
dir = control[:path]
|
34
|
+
Dir.mkdir(dir) unless Dir.exist?(dir)
|
35
|
+
return unless File.writable?(dir)
|
36
|
+
|
37
|
+
delay = control[:delay]
|
38
|
+
@_thread = Contrast::Agent::Thread.new do
|
39
|
+
logger.info("HEAP DUMP THREAD INITIALIZED. WAITING #{ delay } SECONDS TO BEGIN.")
|
40
|
+
sleep(delay)
|
41
|
+
capture_heap_dump
|
76
42
|
end
|
43
|
+
rescue StandardError => e
|
44
|
+
logger.info(LOG_ERROR_DUMPS, e)
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def log_enabled_warning
|
49
|
+
control = Contrast::Utils::HeapDumpUtil.control
|
50
|
+
dir = control[:path]
|
51
|
+
window = control[:window]
|
52
|
+
count = control[:count]
|
53
|
+
delay = control[:delay]
|
54
|
+
clean = control[:clean]
|
55
|
+
|
56
|
+
logger.info <<~WARNING
|
57
|
+
*****************************************************
|
58
|
+
******** HEAP DUMP HAS BEEN ENABLED ********
|
59
|
+
*** APPLICATION PROCESS WILL EXIT UPON COMPLETION ***
|
60
|
+
*****************************************************
|
61
|
+
|
62
|
+
Heap dump is a debugging tool that snapshots the entire
|
63
|
+
state of the Ruby VM. It is an exceptionally expensive
|
64
|
+
process, and should only be used to debug especially
|
65
|
+
pernicious errors.
|
66
|
+
|
67
|
+
It will write multiple memory snaphots, which are liable
|
68
|
+
to be multiple gigabytes in size.
|
69
|
+
They will be named "[unix timestamp]-heap.dump",
|
70
|
+
e.g.: 1020304050-heap.dump
|
71
|
+
|
72
|
+
It will then call Ruby `exit()`.
|
73
|
+
|
74
|
+
If this is not your specific intent, you can (and should)
|
75
|
+
disable this option in your Contrast config file.
|
76
|
+
|
77
|
+
HEAP DUMP PARAMETERS:
|
78
|
+
\t[write files to this directory] dir: #{ dir }
|
79
|
+
\t[wait this many seconds in between dumps] window: #{ window }
|
80
|
+
\t[heap dump this many times] count: #{ count }
|
81
|
+
\t[wait this many seconds into app lifetime] delay: #{ delay }
|
82
|
+
\t[perform gc pass before dump] clean: #{ clean }
|
83
|
+
|
84
|
+
*****************************************************
|
85
|
+
******** YOU HAVE BEEN WARNED ********
|
86
|
+
*****************************************************
|
87
|
+
WARNING
|
88
|
+
end
|
89
|
+
|
90
|
+
def capture_heap_dump
|
91
|
+
control = Contrast::Utils::HeapDumpUtil.control
|
92
|
+
dir = control[:path]
|
93
|
+
window = control[:window]
|
94
|
+
count = control[:count]
|
95
|
+
clean = control[:clean]
|
96
|
+
logger.info('HEAP DUMP MAIN LOOP')
|
97
|
+
ObjectSpace.trace_object_allocations_start
|
98
|
+
count.times do |i|
|
99
|
+
logger.info('STARTING HEAP DUMP PASS', current_pass: i, max: count)
|
100
|
+
snapshot_heap(dir, clean)
|
101
|
+
logger.info('FINISHING HEAP DUMP PASS', current_pass: i, max: count)
|
102
|
+
sleep(window)
|
103
|
+
end
|
104
|
+
ensure
|
105
|
+
ObjectSpace.trace_object_allocations_stop
|
106
|
+
logger.info('*****************************************************')
|
107
|
+
logger.info('******** HEAP DUMP HAS CONCLUDED ********')
|
108
|
+
logger.info('*** APPLICATION PROCESS WILL EXIT SHORTLY ***')
|
109
|
+
logger.info('*****************************************************')
|
110
|
+
exit # rubocop:disable Rails/Exit We weren't kidding!
|
111
|
+
end
|
77
112
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
output = "#{ Time.now.to_f }-heap.dump"
|
88
|
-
output = File.join(dir, output)
|
89
|
-
begin
|
90
|
-
logger.info('OPENING HEADUMP FILE', dir: dir, file: output)
|
91
|
-
file = File.new(output, FILE_WRITE_FLAGS)
|
92
|
-
if clean
|
93
|
-
logger.info('PERFORMING GARBAGE COLLECTION BEFORE HEAP DUMP')
|
94
|
-
GC.start
|
95
|
-
end
|
96
|
-
ObjectSpace.dump_all(output: file)
|
97
|
-
logger.info('FINISHING HEAP DUMP PASS', current_pass: i + 1, max: count)
|
98
|
-
ensure
|
99
|
-
file.close
|
100
|
-
end
|
101
|
-
sleep(window)
|
113
|
+
def snapshot_heap dir, clean
|
114
|
+
output = "#{ Time.now.to_f }-heap.dump"
|
115
|
+
output = File.join(dir, output)
|
116
|
+
begin
|
117
|
+
logger.info('OPENING HEADUMP FILE', dir: dir, file: output)
|
118
|
+
file = File.new(output, FILE_WRITE_FLAGS)
|
119
|
+
if clean
|
120
|
+
logger.info('PERFORMING GARBAGE COLLECTION BEFORE HEAP DUMP')
|
121
|
+
GC.start
|
102
122
|
end
|
123
|
+
ObjectSpace.dump_all(output: file)
|
103
124
|
ensure
|
104
|
-
|
105
|
-
logger.info('*****************************************************')
|
106
|
-
logger.info('******** HEAP DUMP HAS CONCLUDED ********')
|
107
|
-
logger.info('*** APPLICATION PROCESS WILL EXIT SHORTLY ***')
|
108
|
-
logger.info('*****************************************************')
|
109
|
-
exit # We weren't kidding!
|
125
|
+
file.close
|
110
126
|
end
|
111
127
|
end
|
112
128
|
end
|