require "set" require "yaml" require_relative "channel" require_relative "processor" module Immunio @agent = nil # Plugins that are enabled by default. Override using the `plugins_enabled` # and `plugins_disabled` configuration settings. DEFAULT_PLUGINS = ["xss", "file_io", "redirect", "sqli", "eval", "shell_command"] CONFIG_FILENAME = "immunio.yml" class Agent include ActiveSupport::Configurable # These configuration accessors will be available via the `config` method. # NB: :key must be accessed using config[:key] and not config.key config_accessor :key, :secret config_accessor :hello_url config_accessor :log_file config_accessor :log_level config_accessor :log_timings config_accessor :log_context_data config_accessor :http_timeout config_accessor :max_send_queue_size config_accessor :max_report_interval config_accessor :min_report_size config_accessor :max_report_size config_accessor :max_report_bytes # These two values control the exponential backoff behaviour during # communication failure. config_accessor :initial_delay_ms config_accessor :max_delay_ms # How long should the Agent wait for the Immunio Service to provide an # initial ruleset. Any "Falsy" value means don't wait at all. config_accessor :ready_timeout # Control which plugins will be enabled on startup. # `plugins_active` contains the default list of plugins. Other plugins # can be added to the list by putting them in `plugins_enabled` or # removed from the list by adding the to `plugins_disabled`. config_accessor :plugins_active config_accessor :plugins_disabled config_accessor :plugins_enabled # Set to `true` to enable automatic reloading of hook handlers from files. config_accessor :dev_mode # Set to `true` to enable lua debugging urls etc. config_accessor :debug_mode # Set to `false` to disable the agent. config_accessor :agent_enabled # Set to an array of safe methods for creating ActiveSupport::SafeBuffers # with script tags. config_accessor :safe_script_tag_contexts # Any settings specified in vm_data are used to override agent # configuration returned from the server. Mostly used for debugging # purposes. config_accessor :vm_data def initialize Immunio.logger.info "Initializing agent version #{VERSION} for process #{Process.pid}" config.key = config.secret = "-default-" config.hello_url = "https://agent.immun.io/" config.log_file = "log/immunio.log" config.log_level = "info" config.log_timings = false config.log_context_data = false config.http_timeout = 30 # seconds config.max_send_queue_size = 500 # messages config.max_report_interval = 10 # seconds config.min_report_size = 25 # messages config.max_report_size = 50 # messages config.max_report_bytes = 1500000 # Just shy of 1.5 megs config.initial_delay_ms = 100 # milliseconds config.max_delay_ms = 10 * 60 * 1000 # milliseconds config.dev_mode = false config.debug_mode = false config.ready_timeout = 0 # Default list of active plugins config.plugins_active = DEFAULT_PLUGINS.to_set # Default to empty lists for enabled and disabled config.plugins_enabled = [] config.plugins_disabled = [] config.agent_enabled = true config.safe_script_tag_contexts = [] config.vm_data = {} # Be sure all config attributes have a type before this call: load_config Immunio::switch_to_real_logger(config.log_file, config.log_level) if !config.agent_enabled then Immunio.logger.info "Agent disabled in config" return end @vmfactory = VMFactory.new(config[:key], config.secret, config.dev_mode, config.debug_mode) @channel = Channel.new(config) @channel.on_sending do @vmfactory.current_state end # Link things together. The vmfactory needs to know about updates # to the code and data, and the channel needs to know when everything # is up to date. have_code = config.dev_mode have_data = false @channel.on_message do |message| case message[:type] when "engine.vm.code.update" # Don't update code in dev_mode unless config.dev_mode @vmfactory.update_code message[:version], message[:code] have_code = true if have_data @channel.set_ready end end when "engine.vm.data.update" @vmfactory.update_data message[:version], message[:data] have_data = true if have_code @channel.set_ready end end end @processor = Processor.new(@channel, @vmfactory, config) end def load_config Immunio.logger.debug "Default configuration: #{config}" # Try loading file from some standard locations. First match is used. locations = [] locations << Rails.root.join("config", CONFIG_FILENAME) if defined?(Rails.root) && Rails.root locations << File.join("config", CONFIG_FILENAME) locations.each do |location| Immunio.logger.debug "Trying to find config file at #{location}" begin realpath = File.realpath(location) # Raises exception if file doesn't exist Immunio.logger.debug "Found config file at #{realpath}" options = YAML.load_file(realpath).symbolize_keys config.update options Immunio.logger.debug "Configuration after loading from file: #{config}" break rescue SystemCallError => e Immunio.logger.debug "Failed to load config: #{e}" end end # Load private config from env vars. # Set the type of the same as set in initialize config.keys.each do |key| if ENV["IMMUNIO_#{key.upcase}"] then new_value = ENV["IMMUNIO_#{key.upcase}"] case config[key] when String config[key] = new_value when Fixnum config[key] = Integer(new_value) when TrueClass, FalseClass config[key] = !(new_value =~ (/^(true|t|yes|y|1)$/i)).nil? when Array config[key] = new_value.split(/[\s,]+/) when Set config[key] = new_value.split(/[\s,]+/).to_set else raise ArgumentError, "Unknown ENV conversion for #{config[key].class}" end end end Immunio.logger.debug "Configuration after evaluating env vars: #{config}" # Remove any requested plugins, then add any requested plugins. config.plugins_active.subtract(config.plugins_disabled) config.plugins_active.merge(config.plugins_enabled) Immunio.logger.info "Active plugins: #{config.plugins_active.to_a}" end def plugin_enabled?(plugin) # Check if the specified `plugin` is enabled based on the Agent config. config.plugins_active.member?(plugin) end def new_request(*args) @processor.new_request(*args) end def finish_request(*args) @processor.finish_request(*args) end def run_hook(*args) @processor.run_hook(*args) if defined? @processor end def run_hook!(*args) @processor.run_hook!(*args) if defined? @processor end def environment=(environment) @processor.environment = environment end end AGENT_INIT_MUTEX = Mutex.new def self.agent return @agent if @agent AGENT_INIT_MUTEX.synchronize do @agent = Agent.new activate_plugins! if @agent.agent_enabled end @agent end def self.new_request(*args) agent.new_request(*args) end def self.finish_request(*args) agent.finish_request(*args) end def self.run_hook(*args) agent.run_hook(*args) end def self.run_hook!(*args) # Don't run hooks if we're starting up the agent and opening a log agent.run_hook!(*args) unless !@agent && args[0] == "io" && args[1] == "open" end # Initialize startup logger now! create_startup_logger end