# Copyright (c) 2015 Sqreen. All Rights Reserved. # Please refer to our terms for more information: https://www.sqreen.io/terms.html require 'sqreen/js/js_service' require 'sqreen/rule_attributes' require 'sqreen/rule_callback' require 'sqreen/condition_evaluator' require 'sqreen/binding_accessor' require 'sqreen/events/remote_exception' module Sqreen module Rules # Exec js callbacks class ExecJSCB < RuleCB class << self # @return [Sqreen::Js::JsService] def js_service Sqreen::Js::JsService.instance end end def initialize(klass, method, rule_hash) super(klass, method, rule_hash) callbacks = @rule[Attrs::CALLBACKS] @conditions = @rule.fetch(Attrs::CONDITIONS, {}) build_runnable(callbacks) unless pre? || post? || failing? raise Sqreen::Exception, 'no JS CB provided' end @executable = ExecJSCB.js_service.prepare(rule_name, @source) @argument_filter = ArgumentFilter.new(rule_hash) end def pre? @js_pre end def post? @js_post end def failing? @js_failing end def pre(inst, args, budget = nil, &_block) Sqreen.log.debug { "#{self.class} pre args: #{args.inspect}" } return unless pre? call_callback('pre', budget, inst, @cb_bas['pre'], args) end def post(rv, inst, args, budget = nil, &_block) Sqreen.log.debug { "#{self.class} post args: #{args.inspect}" } return unless post? call_callback('post', budget, inst, @cb_bas['post'], args, rv) end def failing(rv, inst, args, budget = nil, &_block) Sqreen.log.debug { "#{self.class} failing args: #{args.inspect}" } return unless failing? call_callback('failing', budget, inst, @cb_bas['failing'], args, rv) end private def record_and_continue?(ret) case ret when NilClass false when Hash ret.keys.each do |k| ret[(begin k.to_sym rescue StandardError k end)] = ret[k] end record_event(ret[:record]) unless ret[:record].nil? unless ret['observations'].nil? ret['observations'].each do |obs| obs[3] = Time.parse(obs[3]) if obs.size >= 3 && obs[3].is_a?(String) record_observation(*obs) end end !ret[:call].nil? else raise Sqreen::Exception, "Invalid return type #{ret.inspect}" end end def call_callback(cb_name, budget, inst, cb_ba_args, args, rv = nil) arguments = cb_ba_args.map do |ba| ba.resolve(binding, framework, inst, args, @data, rv) end arguments = @argument_filter.filter(cb_name, arguments) ret = @executable.run_js_cb(cb_name, budget, arguments) unless record_and_continue?(ret) return nil if ret.nil? return advise_action(ret[:status], ret) end name = ret[:call] rv = ret[:data] new_ba_args = if ret[:args] self.class.build_accessors(ret[:args]) else @cb_bas[name] || [] end # XXX: budgets was not subtracted from call_callback(name, budget, inst, new_ba_args, args, rv) rescue StandardError => e Sqreen.log.warn { "Caught JS callback exception: #{e.inspect}" } Sqreen.log.debug e.backtrace record_exception(e, :cb => cb_name, :args => arguments) nil end def self.build_accessors(reqs) reqs.map do |req| BindingAccessor.new(req, true) end end def build_runnable(callbacks) @cb_bas = {} @source = '' @js_pre = !callbacks['pre'].nil? @js_post = !callbacks['post'].nil? @js_failing = !callbacks['failing'].nil? callbacks.each do |name, args_or_func| @source << "this['#{name.tr("'", "\\'")}'] = " if args_or_func.is_a?(Array) args_or_func = args_or_func.dup @source << args_or_func.pop @cb_bas[name] = self.class.build_accessors(args_or_func) else @source << args_or_func @cb_bas[name] = [] end @source << ";\n" end end end class ArgumentFilter MAX_DEPTH = 2 def initialize(rule) @conditions = rule.fetch(Attrs::CONDITIONS, {}) build_arg_requirements rule end def filter(cbname, arguments) condition = @conditions[cbname] return arguments if condition.nil? || @ba_expressions[cbname].nil? each_hash_val_include(condition) do |needle, haystack, min_length| # We could actually run the binding accessor expression here. needed_idx = @ba_expressions[cbname].index(needle) next unless needed_idx haystack_idx = @ba_expressions[cbname].index(haystack) next unless haystack_idx arguments[haystack_idx] = ArgumentFilter.hash_val_included( arguments[needed_idx], arguments[haystack_idx], min_length.to_i, MAX_DEPTH ) end arguments end def build_arg_requirements(rule) @ba_expressions = {} callbacks = rule[Attrs::CALLBACKS] callbacks.each do |name, args_or_func| next unless args_or_func.is_a?(Array) args_bas = args_or_func[0..-2] unless args_or_func.empty? @ba_expressions[name] = ExecJSCB.build_accessors(args_bas).map(&:expression) end end private def each_hash_val_include(condition, depth = 10) return if depth <= 0 condition.each do |key, values| if key == ConditionEvaluator::HASH_INC_OPERATOR yield values else values.map do |v| each_hash_val_include(v, depth - 1) { |vals| yield vals } if v.is_a?(Hash) end end end end def self.hash_val_included(needed, haystack, min_length = 8, max_depth = 20) new_obj = {} insert = [] to_do = haystack.map { |k, v| [new_obj, k, v, 0] } until to_do.empty? where, key, value, deepness = to_do.pop safe_key = key.is_a?(Integer) ? key : key.to_s if value.is_a?(Hash) && deepness < max_depth val = {} insert << [where, safe_key, val] to_do += value.map { |k, v| [val, k, v, deepness + 1] } elsif value.is_a?(Array) && deepness < max_depth val = [] insert << [where, safe_key, val] i = -1 to_do += value.map { |v| [val, i += 1, v, deepness + 1] } elsif deepness >= max_depth # if we are after max_depth don't try to filter insert << [where, safe_key, value] else v = value.to_s if v.size >= min_length && ConditionEvaluator.str_include?(needed.to_s, v) case where when Array where << value else where[safe_key] = value end end end end insert.reverse.each do |wh, ikey, ival| case wh when Array wh << ival unless ival.respond_to?(:empty?) && ival.empty? else wh[ikey] = ival unless ival.respond_to?(:empty?) && ival.empty? end end new_obj end end end end