class Sqreen::Legacy::Instrumentation
Constants
- FAILING_CB
- OVERTIME_METRIC
- POST_CB
- PRE_CB
Attributes
metrics_engine[RW]
Public Class Methods
callback_wrapper_failing(callbacks, framework, budget, exception, _klass, method, instance, args, &block)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 188 def self.callback_wrapper_failing(callbacks, framework, budget, exception, _klass, method, instance, args, &block) all_start = Sqreen.time #Instrumentation.guard_call(method, []) do returns = [] callbacks.each do |cb| next if cb.whitelisted? rule = cb.rule_name if cb.respond_to?(:rule_name) if budget && budget <= 0.0 next if cb.overtimeable end Sqreen.log.debug { "running failing cb #{cb}" } begin start = Sqreen.time res = cb.failing(exception, instance, args, budget, &block) stop = Sqreen.time Sqreen.log.debug { "ran failing cb #{cb} => #{res.inspect}" } if budget budget -= (stop - start) cb.overtime! if budget <= 0.0 end all_start += (stop - start) if !res.nil? && cb.respond_to?(:block) && (!cb.block && !Sqreen.config_get(:block_all_rules)) Sqreen.log.debug do "#{cb} cannot block, overriding return value" end res = nil elsif res.is_a?(Hash) res[:rule_name] = rule end returns << res rescue StandardError => e Sqreen.log.warn { "we catch an exception: #{e.inspect}" } Sqreen.log.debug { e.backtrace } if cb.respond_to?(:record_exception) cb.record_exception(e) else Sqreen::RemoteException.record(e) end next end Sqreen::PerformanceNotifications.notify(rule || cb.class.name, FAILING_CB, start, stop) end all_stop = Sqreen.time if framework && budget && framework.remaining_perf_budget framework.remaining_perf_budget = budget - (all_stop - all_start) end Sqreen::PerformanceNotifications.notify('hooks_failing', FAILING_CB, all_start, all_stop) returns # end rescue StandardError => e Sqreen.log.warn "we caught an exception between cbs: #{e.inspect}" Sqreen::RemoteException.record(e) [] end
callback_wrapper_post(callbacks, framework, budget, _klass, method, return_val, instance, args, &block)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 133 def self.callback_wrapper_post(callbacks, framework, budget, _klass, method, return_val, instance, args, &block) all_start = Sqreen.time #Instrumentation.guard_call(method, []) do returns = [] callbacks.reverse_each do |cb| next if cb.whitelisted? rule = cb.rule_name if cb.respond_to?(:rule_name) if budget && budget <= 0.0 next if cb.overtimeable end Sqreen.log.debug { "running post cb #{cb}" } begin start = Sqreen.time res = cb.post(return_val, instance, args, budget, &block) stop = Sqreen.time Sqreen.log.debug { "ran post cb #{cb} => #{res.inspect}" } if budget budget -= (stop - start) cb.overtime! if budget <= 0.0 end all_start += (stop - start) if !res.nil? && cb.respond_to?(:block) && (!cb.block && !Sqreen.config_get(:block_all_rules)) Sqreen.log.debug do "#{cb} cannot block, overriding return value" end res = nil elsif res.is_a?(Hash) res[:rule_name] = rule end returns << res rescue StandardError => e Sqreen.log.warn { "we catch an exception: #{e.inspect}" } Sqreen.log.debug { e.backtrace } if cb.respond_to?(:record_exception) cb.record_exception(e) else Sqreen::RemoteException.record(e) end next end Sqreen::PerformanceNotifications.notify(rule || cb.class.name, POST_CB, start, stop) end all_stop = Sqreen.time if framework && budget && framework.remaining_perf_budget framework.remaining_perf_budget = budget - (all_stop - all_start) end Sqreen::PerformanceNotifications.notify('hooks_post', POST_CB, all_start, all_stop) returns #end rescue StandardError => e Sqreen.log.warn { "we caught an exception between cbs: #{e.inspect}" } Sqreen::RemoteException.record(e) [] end
callback_wrapper_pre(callbacks, framework, budget, _klass, method, instance, args, &block)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 76 def self.callback_wrapper_pre(callbacks, framework, budget, _klass, method, instance, args, &block) all_start = Sqreen.time #Instrumentation.guard_call(method, []) do returns = [] callbacks.each do |cb| # If record_request is part of callbacks we should filter after it ran next if cb.whitelisted? rule = cb.rule_name if cb.respond_to?(:rule_name) if budget && budget <= 0.0 next if cb.overtimeable end Sqreen.log.debug { "running pre cb #{cb}" } begin start = Sqreen.time res = cb.pre(instance, args, budget, &block) stop = Sqreen.time Sqreen.log.debug { "ran pre cb #{cb} => #{res.inspect}" } # The first few pre callbacks could not have a request & hence a budget just yet so we try harder to find it budget = framework.remaining_perf_budget if framework && !budget && Sqreen.performance_budget if budget budget -= (stop - start) cb.overtime! if budget <= 0.0 end all_start += (stop - start) if !res.nil? && cb.respond_to?(:block) && (!cb.block && !Sqreen.config_get(:block_all_rules)) Sqreen.log.debug do "#{cb} cannot block, overriding return value" end res = nil elsif res.is_a?(Hash) res[:rule_name] = rule end returns << res break if res.is_a?(Hash) && res[:skip_rem_cbs] rescue StandardError => e Sqreen.log.warn { "we catch an exception: #{e.inspect}" } Sqreen.log.debug { e.backtrace } if cb.respond_to?(:record_exception) cb.record_exception(e) else Sqreen::RemoteException.record(e) end next end Sqreen::PerformanceNotifications.notify(rule || cb.class.name, PRE_CB, start, stop) end all_stop = Sqreen.time framework.remaining_perf_budget = budget - (all_stop - all_start) if framework && budget Sqreen::PerformanceNotifications.notify('hooks_pre', PRE_CB, all_start, all_stop) returns #end rescue StandardError => e Sqreen.log.warn { "we caught an exception between cbs: #{e.inspect}" } Sqreen::RemoteException.record(e) [] end
callbacks()
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 69 def self.callbacks @@registered_callbacks end
define_callback_method(meth, original_meth, klass_name)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 260 def self.define_callback_method(meth, original_meth, klass_name) @sqreen_multi_instr ||= nil proc do |*args, &block| Sqreen.log.debug { "Calling instrumented #{klass_name} #{original_meth} => #{meth}" } budget = nil skip_call = Thread.current[:sqreen_in_use] begin if !skip_call && Sqreen.performance_budget # Not using framework here to try to get a bit more perf by not loading cbs budget = Sqreen::SharedStorage.get(:performance_budget) skip_call = budget && budget <= 0.0 && !@@unovertimable_hookpoints.include?([klass_name, meth]) end rescue StandardError => e Sqreen.log.warn "we catched an exception looking for overtime: #{e.inspect}" Sqreen::RemoteException.record(e) end if !skip_call && Process.pid != Instrumentation.instrumented_pid Sqreen.log.debug do "Instrumented #{Instrumentation.instrumented_pid} != PID #{Process.pid}" end skip_call = true end # If we are already overbudget let's not work at all return __send__(original_meth, *args, &block) if skip_call Instrumentation.guard_multi_call(self, meth, original_meth, args, block) do precbs, postcbs, failcbs = Instrumentation.callbacks.get(klass_name, meth) Thread.current[:sqreen_in_use] = true begin if budget && budget <= 0.0 precbs = precbs.reject(&:overtimeable) if precbs end # We need the framework to set budget remaining time else don't look for it if budget cb_with_framework = nil cb_with_framework = precbs.find(&:framework) if precbs cb_with_framework = postcbs.find(&:framework) unless cb_with_framework || !postcbs cb_with_framework = failcbs.find(&:framework) unless cb_with_framework || !failcbs framework = cb_with_framework ? cb_with_framework.framework : nil else framework = nil end rescue StandardError => e Sqreen.log.warn "we catched an exception looking for framework: #{e.inspect}" Sqreen::RemoteException.record(e) framework = nil end skip = false result = nil if precbs && !precbs.empty? # pre callback returns = Instrumentation.callback_wrapper_pre(precbs, framework, budget, klass_name, meth, self, args, &block) returns.each do |ret| next unless ret.is_a? Hash case ret[:status] when :skip, 'skip' skip = true result = ret[:new_return_value] if ret.key? :new_return_value next when :modify_args, 'modify_args' args = ret[:args] when :raise, 'raise' Thread.current[:sqreen_in_use] = false raise ret[:exception] if ret.key?(:exception) raise Sqreen::AttackBlocked, "Sqreen blocked a security threat (type: #{ret[:rule_name]}). No action is required." end end end Thread.current[:sqreen_in_use] = false return result if skip begin result = __send__(original_meth, *args, &block) rescue StandardError => e Thread.current[:sqreen_in_use] = true budget = Sqreen.performance_budget && framework && framework.remaining_perf_budget failcbs = failcbs.reject(&:overtimeable) if failcbs && budget && budget <= 0.0 raise e unless failcbs && !failcbs.empty? returns = Instrumentation.callback_wrapper_failing(failcbs, framework, budget, e, klass_name, meth, self, args, &block) will_retry = false will_raise = returns.empty? returns.each do |ret| will_raise = true if ret.nil? next unless ret.is_a? Hash case ret[:status] when :override, 'override' result = ret[:new_return_value] if ret.key? :new_return_value when :retry, 'retry' will_retry = true else # :reraise, 'reraise' will_raise = true end end raise e if will_raise retry if will_retry result else Thread.current[:sqreen_in_use] = true budget = Sqreen.performance_budget && framework && framework.remaining_perf_budget postcbs = postcbs.reject(&:overtimeable) if postcbs && budget && budget <= 0.0 return result unless postcbs && !postcbs.empty? # post callback returns = Instrumentation.callback_wrapper_post(postcbs, framework, budget, klass_name, meth, result, self, args, &block) returns.each do |ret| next unless ret.is_a? Hash case ret[:status] when :raise, 'raise' raise ret[:exception] if ret.key?(:exception) raise Sqreen::AttackBlocked, "Sqreen blocked a security threat (type: #{ret[:rule_name]}). No action is required." when :override, 'override' result = ret[:new_return_value] else next end end result ensure Thread.current[:sqreen_in_use] = false end end end # end of proc end
guard_multi_call(instance, method, original_method, args, block) { || ... }
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 243 def self.guard_multi_call(instance, method, original_method, args, block) return yield unless @@overriden_singleton_methods key = [method] args.each { |e| key.push(e.object_id) } if key && @sqreen_multi_instr && @sqreen_multi_instr[instance.object_id].member?(key) return instance.__send__(original_method, *args, &block) end @sqreen_multi_instr ||= Hash.new { |h, k| h[k] = Set.new } # TODO: this should probably be a thread local @sqreen_multi_instr[instance.object_id].add(key) r = yield return r ensure if @sqreen_multi_instr && @sqreen_multi_instr[instance.object_id] && @sqreen_multi_instr[instance.object_id].delete(key).empty? @sqreen_multi_instr.delete(instance.object_id) end end
instrumented_pid()
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 65 def self.instrumented_pid @@instrumented_pid end
new(metrics_engine = nil)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 774 def initialize(metrics_engine = nil) self.metrics_engine = metrics_engine return if metrics_engine.nil? ### init metric to count calls to sqreen metrics_engine.create_metric('name' => CallCountable::COUNT_CALLS, 'period' => 60, 'kind' => 'Sum') ### init metric to count request whitelist matches (ip or path whitelist) metrics_engine.create_metric('name' => Sqreen::Rules::RecordRequestContext::WHITELISTED_METRIC, 'period' => 60, 'kind' => 'Sum') ### init metric to count over budget hits metrics_engine.create_metric('name' => OVERTIME_METRIC, 'period' => 60, 'kind' => 'Sum') end
overriden()
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 73 def self.overriden @@overriden_methods end
request_hookpoint_method(original_meth)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 406 def self.request_hookpoint_method(original_meth) proc do |*args, &block| has_notifications = Sqreen::PerformanceNotifications.listen_for? Sqreen::PerformanceNotifications::BinnedMetrics.start_request if has_notifications begin __send__(original_meth, *args, &block) ensure if has_notifications Sqreen::PerformanceNotifications.instrument('next_req_notifs', PRE_CB) do Sqreen::PerformanceNotifications::LogPerformance.next_request Sqreen::PerformanceNotifications::NewRelic.next_request Sqreen::PerformanceNotifications::BinnedMetrics.finish_request end end end end end
semaphore()
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 61 def self.semaphore @@override_semaphore end
Public Instance Methods
add_callback(cb)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 594 def add_callback(cb) @@override_semaphore.synchronize do klass = cb.klass method = cb.method key = [klass, method] if (call_count = ENV['SQREEN_DEBUG_CALL_COUNT']) call_count = JSON.parse(call_count) if cb.respond_to?(:rule_name) && call_count.key?(cb.rule_name) count = call_count[cb.rule_name] Sqreen.log.debug { "override rule:#{cb.rule_name} call_count:#{count.inspect}" } cb.instance_eval { @call_count_interval = call_count[cb.rule_name] } end end @@record_request_hookpoints << key if cb.is_a?(Sqreen::Rules::RecordRequestContext) already_overriden = @@overriden_methods.include? key if !already_overriden if is_class_method?(klass, method) Sqreen.log.debug { "overriding class method for #{cb}" } success = override_class_method(klass, method) elsif is_instance_method?(klass, method) Sqreen.log.debug { "overriding instance method for #{cb}" } success = override_instance_method(klass, method) else # FIXME: Override define_method and other dynamic ways to # The following should be monitored to make sure we # don't forget dynamically added methods: # - define_method # - method_added # - method_missing # ... # msg = "#{cb} is neither class or instance" raise Sqreen::NotImplementedYet, msg end @@overriden_methods += [key] if success else Sqreen.log.debug { "#{key} was already overriden" } end if klass != Object && klass != Kernel && !Sqreen.features['instrument_all_instances'] && !defined?(::JRUBY_VERSION) insts = 0 ObjectSpace.each_object(klass) do |e| next if e.is_a?(Class) || e.is_a?(Module) next unless e.singleton_methods.include?(method.to_sym) insts += 1 if override_singleton_method(e, klass, method) end if insts > 0 Sqreen.log.debug { "Reinstrumented #{insts} instances of #{klass}" } end end Sqreen.log.debug { "Adding callback #{cb} for #{klass} #{method}" } @@registered_callbacks.add(cb) @@unovertimable_hookpoints << key unless cb.overtimeable @@instrumented_pid = Process.pid end end
adjust_method_name(method)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 540 def adjust_method_name(method) method.to_s end
get_saved_method_name(meth, suffix = nil)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 473 def get_saved_method_name(meth, suffix = nil) "#{meth}_sq#{suffix}_not_modified".to_sym end
hardcoded_callbacks(framework)
click to toggle source
@return [Array<Sqreen::CB>]
# File lib/sqreen/legacy/instrumentation.rb, line 724 def hardcoded_callbacks(framework) [ ### callback for performing sec responses based on ip ### init redefined to implement smartass way to hook it upon the ### framework's middleware #call Sqreen::Rules::RunReqStartActions.new(framework), ### callback for performing sec responses based on user Sqreen::Rules::RunUserActions.new(Sqreen, :identify, 0), ### callback for performing sec responses based on user Sqreen::Rules::RunUserActions.new(Sqreen, :auth_track, 1), ] end
instrument!(rules, framework)
click to toggle source
Instrument the application code using the rules @param rules [Array<Hash>] Rules
to instrument @param framework [Sqreen::Frameworks::GenericFramework]
# File lib/sqreen/legacy/instrumentation.rb, line 740 def instrument!(rules, framework) ### set up rule signature verifier verifier = nil if Sqreen.features['rules_signature'] && Sqreen.config_get(:rules_verify_signature) == true && !defined?(::JRUBY_VERSION) verifier = Sqreen::SqreenSignedVerifier.new else Sqreen.log.debug('Rules signature is not enabled') end ### force clean instrumentation callback list remove_all_callbacks # Force cb tree to be empty before instrumenting ### for each rule description, transform into format for adding callback rules.each do |rule| rcb = Sqreen::Rules.cb_from_rule(rule, self, metrics_engine, verifier) next unless rcb ### attach framework to callback rcb.framework = framework ### add callback add_callback(rcb) end # add hardcoded callbacks, observing priority hardcoded_callbacks(framework).each { |cb| add_callback(cb) } ### globally declare instrumentation ready ### from within instance method? not even thread local? Sqreen.instrumentation_ready = true Sqreen.log.info('Instrumentation activated') end
is_class_method?(klass, method)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 555 def is_class_method?(klass, method) method = adjust_method_name(method) klass.singleton_methods.include? method end
is_instance_method?(klass, method)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 549 def is_instance_method?(klass, method) method = adjust_method_name(method) klass.instance_methods.include?(method) || klass.private_instance_methods.include?(method) end
override_class_method(klass, meth)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 425 def override_class_method(klass, meth) original = meth.to_sym saved_meth_name = get_saved_method_name(meth).to_sym new_method = "#{meth}_modified".to_sym klass.singleton_class.instance_eval do alias_method saved_meth_name, original p = Instrumentation.define_callback_method(original, saved_meth_name, klass) define_method(new_method, p) private new_method method_kind = nil if public_method_defined?(original) method_kind = :public elsif protected_method_defined?(original) method_kind = :protected elsif private_method_defined?(original) method_kind = :private end alias_method original, new_method __send__(method_kind, original) private saved_meth_name end end
override_instance_method(klass_name, meth)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 477 def override_instance_method(klass_name, meth) saved_meth_name = get_saved_method_name(meth) new_method = "#{meth}_modified".to_sym # do not include ancestors o/wise we might get a saved method from a # superclass which is never called because the original name # (instrumented) method of the superclass has been overridden private_meths = klass_name.private_instance_methods(false) if private_meths.include?(saved_meth_name) Sqreen.log.debug { "#{saved_meth_name} found #{klass_name}##{meth} already instrumented" } return saved_meth_name end p = Instrumentation.define_callback_method(meth, saved_meth_name, klass_name) method_kind = nil klass_name.class_eval do alias_method saved_meth_name, meth define_method(new_method, p) if @@record_request_hookpoints.include?([klass_name, meth]) p = Instrumentation.request_hookpoint_method(new_method) new_method = "#{new_method}_req_hp_wrapper" define_method(new_method, p) end if public_method_defined?(meth) method_kind = :public elsif protected_method_defined?(meth) method_kind = :protected elsif private_method_defined?(meth) method_kind = :private end alias_method meth, new_method private saved_meth_name private new_method __send__(method_kind, meth) end saved_meth_name end
override_singleton_method(instance, klass_name, meth)
click to toggle source
is that actually used? if so, do not attempt to alter frozen instances
Override a singleton method on an instance
# File lib/sqreen/legacy/instrumentation.rb, line 570 def override_singleton_method(instance, klass_name, meth) @@overriden_singleton_methods = true saved_meth_name = get_saved_method_name(meth, 'singleton') if instance.respond_to?(saved_meth_name, true) Sqreen.log.debug { "#{saved_meth_name} found #{instance.class}##{instance.object_id} already instrumented" } return nil elsif instance.frozen? Sqreen.log.debug { "#{instance.class}##{instance.object_id} is frozen, not reinstrumenting" } return nil end raise Sqreen::NotImplementedYet, "#{instance.inspect} doesn't respond to define_singleton_method" unless instance.respond_to?(:define_singleton_method) p = Instrumentation.define_callback_method(meth, saved_meth_name, klass_name) instance.define_singleton_method(saved_meth_name, instance.method(meth)) instance.define_singleton_method(meth, p) # Hide saved method (its only available in this syntax) eval <<-RUBY, binding, __FILE__, __LINE__ + 1 class << instance private :#{saved_meth_name} end saved_meth_name RUBY end
remove_all_callbacks()
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 711 def remove_all_callbacks @@override_semaphore.synchronize do @@registered_callbacks.entries.each do |cb| remove_callback_no_lock(cb) end Sqreen.instrumentation_ready = false Sqreen.log.info('Instrumentation deactivated') end end
remove_callback(cb)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 657 def remove_callback(cb) @@override_semaphore.synchronize do remove_callback_no_lock(cb) end end
remove_callback_no_lock(cb)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 663 def remove_callback_no_lock(cb) klass = cb.klass method = cb.method key = [klass, method] already_overriden = @@overriden_methods.include? key unless already_overriden Sqreen.log.debug { "#{key} apparently not overridden" } end defined_cbs = @@registered_callbacks.get(klass, method).flatten nb_removed = 0 defined_cbs.each do |found_cb| if found_cb == cb Sqreen.log.debug { "Removing callback #{found_cb}" } @@registered_callbacks.remove(found_cb) nb_removed += 1 else Sqreen.log.debug { "Not removing callback #{found_cb} (remains #{defined_cbs.size} cbs)" } end end return unless nb_removed == defined_cbs.size Sqreen.log.debug { "Removing overriden method #{key}" } @@overriden_methods.delete(key) if is_class_method?(klass, method) unoverride_class_method(klass, method) elsif is_instance_method?(klass, method) unoverride_instance_method(klass, method) else ### Module#prepend will take care of that # FIXME: Override define_method and other dynamic ways to # The following should be monitored to make sure we # don't forget dynamically added methods: # - define_method # - method_added # - method_missing # ... # msg = "#{cb} is neither singleton or instance" raise Sqreen::NotImplementedYet, msg end end
unoverride_class_method(klass, meth)
click to toggle source
bad idea anyway
WARNING We do not actually remove `meth`
# File lib/sqreen/legacy/instrumentation.rb, line 521 def unoverride_class_method(klass, meth) saved_meth_name = get_saved_method_name(meth) method_kind = nil klass.singleton_class.instance_eval do if public_method_defined?(meth.to_sym) method_kind = :public elsif protected_method_defined?(original) method_kind = :protected elsif private_method_defined?(meth.to_sym) method_kind = :private end alias_method meth.to_sym, saved_meth_name.to_sym __send__(method_kind, meth.to_sym) end end
unoverride_instance_method(obj, meth)
click to toggle source
# File lib/sqreen/legacy/instrumentation.rb, line 453 def unoverride_instance_method(obj, meth) saved_meth_name = get_saved_method_name(meth) method_kind = nil obj.class_eval do # Note: As a lambda the following will crash ruby 2.2.3p173 if public_method_defined?(meth) method_kind = :public elsif protected_method_defined?(meth) method_kind = :protected elsif private_method_defined?(meth) method_kind = :private end alias_method meth, saved_meth_name __send__(method_kind, meth) remove_method saved_meth_name end end
valid_method?(obj, method)
click to toggle source
Does this object or an instance of it respond_to method?
# File lib/sqreen/legacy/instrumentation.rb, line 561 def valid_method?(obj, method) return true if is_class_method?(obj, method) return false unless obj.respond_to?(:instance_methods) is_instance_method?(obj, method) end