# 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' module Sqreen # Implements actions (behavior taken in response to agent signals) module Actions # Exception for when an unknown action type is gotten from the server class UnknownActionType < ::Sqreen::Exception attr_reader :action_type def initialize(action_type) super("no such action type: #{action_type}. Must be one of #{Base.known_types}") @action_type = action_type end end # Where the currently loaded actions are stored. Singleton class Repository include Singleton def add(params, action) action.class.index(params || {}, action) end 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 clear Base.known_subclasses.each(&:clear) end end # @return [Sqreen::Actions::Base] def self.deserialize_action(hash) action_type = hash['action'] raise 'no action type available' unless action_type subclass = Base.get_type_class(action_type) raise UnknownActionType, action_type unless subclass id = hash['action_id'] raise 'no action id available' unless id duration = hash['duration'] if !duration.nil? && duration <= 0 Sqreen.log.debug "Action #{id} is already expired" return nil end opts = { :duration => duration, :send_response => hash['send_response'], } 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 duration = opts[:duration] @expiry = Time.new + duration unless duration.nil? @send_response = if opts[:send_response].nil? true else !!opts[:send_response] end end # See Sqreen::CB for return values def run(*args) return if expiry && Time.new > expiry ret = do_run *args unless ret.nil? || !@send_response Sqreen.internal_track(event_name, 'properties' => { 'output' => event_properties(*args), 'action_id' => id, }) end ret end protected def do_run(*_args) raise ::Sqreen::NotImplementedYet, "do_run not implemented in #{self.class}" # implement in subclasses end def event_properties(*_run_args) raise ::Sqreen::NotImplementedYet, "event_properties not implemented in #{self.class}" # implement in subclasses end private def event_name "sq.action.#{self.class.type_name}" end @@subclasses = {} class << self private :new attr_reader :type_name 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 protected def type_name=(name) @type_name = name @@subclasses[name] = self end end end 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 end end # Block a list of IP address ranges. Standard "raise" behavior. class BlockIp < Base 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) end def do_run(client_ip) e = Sqreen::AttackBlocked.new("Blocked client's IP #{client_ip} " \ "(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 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 end def do_run(client_ip) Sqreen.log.info "Will request redirect for client with IP #{client_ip} " \ "(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 } end end # 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) end def do_run(identity_params) Sqreen.log.info( "Will raise due to user being blocked by action #{id}. " \ "Blocked user identity: #{identity_params}" ) e = Sqreen::AttackBlocked.new( "Blocked user with identity #{identity_params} " \ 'due to automatic security response. No action is required' ) { :status => :raise, :exception => e, } end def event_properties(identity_params) { 'user' => identity_params } end end end end