# 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' 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 subclass.new(id, duration, hash['parameters'] || {}) end class Base attr_reader :id, :expiry def initialize(id, duration) @id = id @expiry = Time.new + duration unless duration.nil? end # See Sqreen::CB for return values def run(*args) return if expiry && Time.new > expiry ret = do_run *args unless ret.nil? Sqreen.internal_track(event_name, 'properties' => event_properties(*args). merge('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, duration, params = {}) super(id, duration) 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, duration, params = {}) super(id, duration) @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 end end