# 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/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 initialize @actions = {} # indexed by subclass @actions.default_proc = proc { |h, k| h[k] = [] } end def <<(action) @actions[action.class] << action end def [](action_class) @actions[action_class] end def clear @actions.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 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_types @@subclasses.keys 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 IpRanges attr_reader :ranges 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) 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 self.type_name = 'block_ip' def initialize(id, opts, params = {}) 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 (action: #{id}). No action is required") { :status => :raise, :exception => e } 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 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}). " { :status => :skip, :new_return_value => [303, { 'Location' => @redirect_url }, ['']], } 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' 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}" ) 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 private def stringify_keys(hash) Hash[ hash.map { |k, v| [k.to_s, v] } ] end end end end