lib/sqreen/actions.rb in sqreen-1.15.0-java vs lib/sqreen/actions.rb in sqreen-1.15.1

- old
+ new

@@ -1,9 +1,10 @@ # Copyright (c) 2018 Sqreen. All Rights Reserved. # Please refer to our terms for more information: https://www.sqreen.io/terms.html require 'ipaddr' +require 'sqreen/trie' require 'sqreen/log' require 'sqreen/exception' require 'sqreen/sdk' require 'sqreen/frameworks' require 'singleton' @@ -22,25 +23,21 @@ # Where the currently loaded actions are stored. Singleton class Repository include Singleton - def initialize - @actions = {} # indexed by subclass - @actions.default_proc = proc { |h, k| h[k] = [] } + def add(params, action) + action.class.index(params || {}, action) end - def <<(action) - @actions[action.class] << action + def get(action_class, key) + action_class = Base.get_type_class(action_class) unless action_class.class == Class + action_class.actions_matching key end - def [](action_class) - @actions[action_class] - end - def clear - @actions.clear + Base.known_subclasses.each(&:clear) end end # @return [Sqreen::Actions::Base] def self.deserialize_action(hash) @@ -66,10 +63,12 @@ subclass.new(id, opts, hash['parameters'] || {}) end # Base class for actions + # subclasses must also implement some methods in their singleton classes + # (actions_matching, index and clear) class Base attr_reader :id, :expiry, :send_response def initialize(id, opts) @id = id @@ -122,14 +121,31 @@ def get_type_class(name) @@subclasses[name] end + def known_subclasses + @@subclasses.values + end + def known_types @@subclasses.keys end + # all actions matching, possibly already expired + def actions_matching(_key) + raise 'implement in singletons of subclasses' + end + + def index(_params, _action) + raise 'implement in singletons of subclasses' + end + + def clear + raise 'implement in singletons of subclasses' + end + def inherited(subclass) class << subclass public :new end end @@ -141,76 +157,107 @@ @@subclasses[name] = self end end end - module IpRanges - attr_reader :ranges + module IpRangesIndex + def add_prefix(prefix_str, data) + @trie_v4 ||= Sqreen::Trie.new + @trie_v6 ||= Sqreen::Trie.new(nil, nil, Socket::AF_INET6) + prefix = Sqreen::Prefix.from_str(prefix_str, data) + trie = prefix.family == Socket::AF_INET6 ? @trie_v6 : @trie_v4 + trie.insert prefix + end + def matching_actions(client_ip) + parsed_ip = IPAddr.new(client_ip) + trie = parsed_ip.family == Socket::AF_INET6 ? @trie_v6 : @trie_v4 + found = trie.search_matching(parsed_ip.to_i, parsed_ip.family) + return [] unless found.size > 0 + + Sqreen.log.debug("Client ip #{client_ip} matches #{found.inspect}") + found.map(&:data) + end + + def clear + @trie_v4 = Sqreen::Trie.new + @trie_v6 = Sqreen::Trie.new(nil, nil, Socket::AF_INET6) + end + end + + module IpRangeIndexedActionClass + include IpRangesIndex + + def actions_matching(client_ip) + matching_actions client_ip + end + + def index(params, action) + ranges = parse_ip_ranges params + + ranges.each do |r| + add_prefix r, action + end + end + + private + + # returns array of prefixes in string form def parse_ip_ranges(params) ranges = params['ip_cidr'] unless ranges && ranges.is_a?(Array) && !ranges.empty? raise 'no non-empty ip_cidr array present' end - @ranges = ranges.map &IPAddr.method(:new) + ranges end - - def matches_ip?(client_ip) - parsed_ip = IPAddr.new client_ip - found = ranges.find { |r| r.include? parsed_ip } - return false unless found - - Sqreen.log.debug("Client ip #{client_ip} matches #{found.inspect}") - true - end end # Block a list of IP address ranges. Standard "raise" behavior. class BlockIp < Base - include IpRanges + extend IpRangeIndexedActionClass + self.type_name = 'block_ip' def initialize(id, opts, params = {}) + # no need to store the ranges for this action, the index filter the class super(id, opts) - parse_ip_ranges params end def do_run(client_ip) - return nil unless matches_ip? client_ip e = Sqreen::AttackBlocked.new("Blocked client's IP #{client_ip} " \ - "(action: #{id} covering range(s) #{ranges}). No action is required") - { :status => :raise, :exception => e } + "(action: #{id}). No action is required") + { :status => :raise, :exception => e, :skip_rem_cbs => true } end def event_properties(client_ip) { 'ip_address' => client_ip } end end # Block a list of IP address ranges by forcefully redirecting the user # to a specific URL. class RedirectIp < Base - include IpRanges + extend IpRangeIndexedActionClass + self.type_name = 'redirect_ip' attr_reader :redirect_url def initialize(id, opts, params = {}) super(id, opts) @redirect_url = params['url'] raise "no url provided for action #{id}" unless @redirect_url - parse_ip_ranges params end def do_run(client_ip) - return nil unless matches_ip? client_ip Sqreen.log.info "Will request redirect for client with IP #{client_ip} " \ - "(action: #{id} covering range(s) #{ranges})." + "(action: #{id})." { :status => :skip, :new_return_value => [303, { 'Location' => @redirect_url }, ['']], + :skip_rem_cbs => true, } end def event_properties(client_ip) { 'ip_address' => client_ip, 'url' => @redirect_url } @@ -220,19 +267,49 @@ # Blocks a user at the point Sqreen::identify() # or Sqreen::auth_track() are called class BlockUser < Base self.type_name = 'block_user' + class << self + def actions_matching(identity_params) + key = stringify_keys(identity_params) + actions = @idx[key] + actions || [] + end + + def index(params, action) + @idx ||= {} + users = params['users'] + raise ::Sqreen::Exception, 'nil "users" param for block_user action' if users.nil? + raise ::Sqreen::Exception, '"users" param must be an array' unless users.is_a? Array + + users.each do |u| + @idx[u] ||= [] + @idx[u] << action + end + end + + def clear + @idx = {} + end + + private + + def stringify_keys(hash) + Hash[ + hash.map { |k, v| [k.to_s, v] } + ] + end + end + + # BlockUser proper definition continues + def initialize(id, opts, params = {}) super(id, opts) - @users = params['users'] - raise ::Sqreen::Exception, 'nil "users" param for block_user action' if @users.nil? - raise ::Sqreen::Exception, '"users" param must be an array' unless @users.is_a? Array end def do_run(identity_params) - return unless @users.include? stringify_keys(identity_params) Sqreen.log.info( "Will raise due to user being blocked by action #{id}. " \ "Blocked user identity: #{identity_params}" ) @@ -247,17 +324,9 @@ } end def event_properties(identity_params) { 'user' => identity_params } - end - - private - - def stringify_keys(hash) - Hash[ - hash.map { |k, v| [k.to_s, v] } - ] end end end end