# Copyright 2015-2017 F-Secure

# Licensed under the Apache License, Version 2.0 (the "License"); you
# may not use this file except in compliance with the License.  You may
# obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.  See the License for the specific language governing
# permissions and limitations under the License.

"""Module for acquiring and analysing memory snapshots of a running VM.

The VolatilityHook for analysing memory snapshots requires volatility.

http://www.volatilityfoundation.org

"""

import os
import libvirt
from threading import Event
from datetime import datetime

from see import Hook
from see.context import PAUSED

from .utils import launch_process, collect_process_output, create_folder


class MemoryHook(Hook):
    """
    Memory snapshotting hook.

    On the given event, it dumps the Context's memory state
    on a file in the given folder.

    configuration::

        {
          "results_folder": "/folder/where/to/store/memory/dump/file",
          "memory_snapshots_on_event": ["event_triggering_memory_snapshot"],
          "compress_snapshots": False,
          "delete_snapshots": False
        }

    "memory_snapshot_on_event" can be either a string or a list of Events.

    If "compress_snapshots" is set to True, the snapshot files will be
    archived as zlib to save space. Default to False.

    If "delete_snapshots" is set to True, the snapshot files will be deleted
    at the end of the execution. Default to False.

    """
    def __init__(self, parameters):
        super().__init__(parameters)
        self.memdumps = []
        self.setup_handlers()

    def setup_handlers(self):
        snapshots = self.configuration.get('memory_snapshots_on_event', ())
        events = isinstance(snapshots, str) and [snapshots] or snapshots

        for event in events:
            self.context.subscribe(event, self.snapshot_handler)
            self.logger.debug("Memory snapshot registered at %s event", event)

    def snapshot_handler(self, event):
        self.logger.debug("Event %s: taking memory snapshot.", event)

        snapshot_path = self.memory_snapshot(event)
        self.context.trigger("memory_snapshot_taken", path=snapshot_path)

        self.logger.info("Memory snapshot %s taken.", snapshot_path)

    def memory_snapshot(self, event):
        folder_path = self.configuration['results_folder']
        file_name = "%s_%s.bin%s" % (
            event,
            datetime.now().replace(microsecond=0).time().strftime("%H%M%S"),
            self.configuration.get('compress_snapshots', False)
            and '.gz' or '')
        snapshot_path = os.path.join(folder_path, file_name)

        create_folder(folder_path)
        self.dump_memory(snapshot_path)

        return snapshot_path

    def dump_memory(self, memory_dump_path):
        self.assert_context_state()
        memory_snapshot(self.context, memory_dump_path,
                        self.configuration.get('compress_snapshots', False))

        self.memdumps.append(memory_dump_path)

    def assert_context_state(self):
        if self.context.domain.state()[0] is not PAUSED:
            raise RuntimeError("Context must be paused during memory snapshot")

    def cleanup(self):
        if self.configuration.get('delete_snapshots', False):
            for memdump in self.memdumps:
                os.remove(memdump)


class VolatilityHook(Hook):
    """
    Volatility post-processor hook.

    On the given event, it runs the given Volatility plugins
    on all the provided memory snapshots.

    The memory snapshots files paths must be communicated through
    a "memory_snapsho_ttaken" event within the path attribute.

    configuration::

        {
          "results_folder": "/folder/where/to/store/memory/dump/file",
          "start_processing_on_event": "event_starting_async_processing",
          "wait_processing_on_event": "event_waiting_async_processing",
          "profile": "Win7SP1x64",
          "plugins": ["mutantscan"]
        }

    On start_processing_on_event, the volatility process will be started
    with the given plugins. All provided memory snapshots will be analysed
    sequentially.
    wait_processing_on_event allows to wait for the asyncronous processes
    to terminate.

    A profile key must be provided with the memory profile of the Sandbox.
    The list of plugins will be passed directly to Volatiliti's multiscan
    command.

    """
    def __init__(self, parameters):
        super().__init__(parameters)
        self.snapshots = []
        self.setup_handlers()
        self.processing_done = Event()

    def setup_handlers(self):
        self.context.subscribe('memory_snapshot_taken',
                               self.memory_snapshot_handler)

        if {'start_processing_on_event',
            'wait_processing_on_event'} <= set(self.configuration):
            event = self.configuration['start_processing_on_event']
            self.context.subscribe_async(event, self.start_processing_handler)
            self.logger.debug("Volatility scheduled at %s event", event)

            event = self.configuration['wait_processing_on_event']
            self.context.subscribe(event, self.stop_processing_handler)
            self.logger.debug("Volatility processing wait at %s event", event)

    def memory_snapshot_handler(self, event):
        if hasattr(event, 'path'):
            self.logger.debug("Event %s: new memory dump %s.",
                              event, event.path)
            self.snapshots.append(event.path)
        else:
            self.logging.warning("%s event received, no path specified.")

    def start_processing_handler(self, event):
        """Asynchronous handler starting the Volatility processes."""
        self.logger.debug("Event %s: starting Volatility process(es).", event)

        for snapshot in self.snapshots:
            self.process_snapshot(snapshot)

        self.processing_done.set()

    def process_snapshot(self, snapshot):
        profile = self.configuration.get('profile', ())

        for plugin in self.configuration.get('plugins', ()):
            try:
                process_memory_snapshot(snapshot, profile, plugin)
            except RuntimeError:
                self.logger.exception("Unable to run %s plugin.", plugin)

    def stop_processing_handler(self, event):
        self.logger.debug("Event %s: waiting for Volatility process(es).",
                          event)
        self.processing_done.wait()
        self.logger.info("Done processing memory with Volatility.")


def memory_snapshot(context, memory_dump_path, compress):
    # fix issue with libvirt's API
    open(memory_dump_path, 'a').close()  # touch file to set permissions

    dump_flag = libvirt.VIR_DUMP_MEMORY_ONLY
    if compress:
        dump_format = libvirt.VIR_DOMAIN_CORE_DUMP_FORMAT_KDUMP_ZLIB
    else:
        dump_format = libvirt.VIR_DOMAIN_CORE_DUMP_FORMAT_RAW

    context.domain.coreDumpWithFormat(memory_dump_path, dump_format, dump_flag)


def process_memory_snapshot(snapshot_path, profile, plugin):
    process = launch_process('volatility',
                             '--profile=%s' % profile,
                             '--filename=%s' % snapshot_path,
                             plugin)
    file_name = '%s_%s.log' % (snapshot_path.split('.')[0], plugin)

    collect_process_output(process, file_name)