Codebase list python-faraday / b74a1af plugins / controller.py
b74a1af

Tree @b74a1af (Download .tar.gz)

controller.py @b74a1afraw · history · blame

#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
Faraday Penetration Test IDE
Copyright (C) 2013  Infobyte LLC (http://www.infobytesec.com/)
See the file 'doc/LICENSE' for the license information

'''
import os
import time
import shlex
import logging
from threading import Thread
from multiprocessing import JoinableQueue, Process

from config.configuration import getInstanceConfiguration
from plugins.plugin import PluginProcess
import model.api
from model.commands_history import CommandRunInformation
from model import Modelactions
from utils.logs import getLogger

from config.constant import (
    CONST_FARADAY_HOME_PATH,
    CONST_FARADAY_ZSH_OUTPUT_PATH)
CONF = getInstanceConfiguration()

logger = logging.getLogger(__name__)


class PluginCommiter(Thread):

    def __init__(self, output_queue, output, pending_actions, plugin, command, mapper_manager, end_event=None):
        super(PluginCommiter, self).__init__()
        self.output_queue = output_queue
        self.pending_actions = pending_actions
        self.stop = False
        self.plugin = plugin
        self.command = command
        self.mapper_manager = mapper_manager
        self.output = output
        self._report_path = os.path.join(CONF.getReportPath(), command.workspace)
        self._report_ppath = os.path.join(self._report_path, "process")
        self._report_upath = os.path.join(self._report_path, "unprocessed")
        self.end_event = end_event

    def stop(self):
        self.stop = True

    def commit(self):
        logger.debug('Plugin end. Commiting to faraday server.')
        self.pending_actions.put(
            (Modelactions.PLUGINEND, self.plugin.id, self.command.getID()))
        self.command.duration = time.time() - self.command.itime
        self.mapper_manager.update(self.command)
        if self.end_event:
            self.end_event.set()

    def run(self):
        name = ''
        try:
            self.output_queue.join()
            self.commit()
            if os.path.isfile(self.output):
                # sometimes output is a filepath
                name = os.path.basename(self.output)
                os.rename(self.output,
                    os.path.join(self._report_ppath, name))
        except Exception as ex:
            logger.exception(ex)
            logger.info('Something failed, moving file to unprocessed')
            os.rename(self.output,
                      os.path.join(self._report_upath, name))



class PluginController(Thread):
    """
    TODO: Doc string.
    """
    def __init__(self, id, plugin_manager, mapper_manager, pending_actions, end_event=None):
        super(PluginController, self).__init__()
        self.plugin_manager = plugin_manager
        self._plugins = plugin_manager.getPlugins()
        self.id = id
        self._actionDispatcher = None
        self._setupActionDispatcher()
        self._mapper_manager = mapper_manager
        self.output_path = os.path.join(
            os.path.expanduser(CONST_FARADAY_HOME_PATH),
            CONST_FARADAY_ZSH_OUTPUT_PATH)
        self._active_plugins = {}
        self.plugin_sets = {}
        self.plugin_manager.addController(self, self.id)
        self.stop = False
        self.pending_actions = pending_actions
        self.end_event = end_event

    def _find_plugin(self, plugin_id):
        return self._plugins.get(plugin_id, None)

    def _is_command_malformed(self, original_command, modified_command):
        """
        Checks if the command to be executed is safe and it's not in the
        block list defined by the user. Returns False if the modified
        command is ok, True if otherwise.
        """
        block_chars = set(["|", "$", "#"])

        if original_command == modified_command:
            return False

        orig_cmd_args = shlex.split(original_command)

        if not isinstance(modified_command, basestring):
            modified_command = ""
        mod_cmd_args = shlex.split(modified_command)

        block_flag = False
        orig_args_len = len(orig_cmd_args)
        for index in xrange(0, len(mod_cmd_args)):
            if (index < orig_args_len and
                    orig_cmd_args[index] == mod_cmd_args[index]):
                continue

            for char in block_chars:
                if char in mod_cmd_args[index]:
                    block_flag = True
                    break

        return block_flag

    def _get_plugins_by_input(self, cmd, plugin_set):
        for plugin in plugin_set.itervalues():
            if plugin.canParseCommandString(cmd):
                return plugin
        return None

    def getAvailablePlugins(self):
        """
        Return a dictionary with the available plugins.
        Plugin ID's as keys and plugin instences as values
        """
        return self._plugins

    def stop(self):
        self.stop = True

    def processOutput(self, plugin, output, command, isReport=False):
        """
            Process the output of the plugin. This will start the PluginProcess
            and also PluginCommiter (thread) that will informa to faraday server
            when the command finished.

        :param plugin: Plugin to execute
        :param output: read output from plugin or term
        :param command_id: command id that started the plugin
        :param isReport: Report or output from shell
        :return: None
        """
        output_queue = JoinableQueue()
        plugin.set_actions_queue(self.pending_actions)

        plugin_process = PluginProcess(
            plugin, output_queue, isReport)

        getLogger(self).debug(
            "Created plugin_process (%d) for plugin instance (%d)" %
            (id(plugin_process), id(plugin)))

        self.pending_actions.put((Modelactions.PLUGINSTART, plugin.id, command.getID()))
        output_queue.put((output, command.getID()))
        plugin_commiter = PluginCommiter(
            output_queue,
            output,
            self.pending_actions,
            plugin,
            command,
            self._mapper_manager,
            self.end_event,
        )
        plugin_commiter.start()
        # This process is stopped when plugin commiter joins output queue
        plugin_process.start()

    def _processAction(self, action, parameters):
        """
        decodes and performs the action given
        It works kind of a dispatcher
        """
        getLogger(self).debug(
            "_processAction - %s - parameters = %s" %
            (action, str(parameters)))
        self._actionDispatcher[action](*parameters)

    def _setupActionDispatcher(self):
        self._actionDispatcher = {
            Modelactions.ADDHOST: model.api.addHost,
            Modelactions.ADDSERVICEHOST: model.api.addServiceToHost,
            #Vulnerability
            Modelactions.ADDVULNHOST: model.api.addVulnToHost,
            Modelactions.ADDVULNSRV: model.api.addVulnToService,
            #VulnWeb
            Modelactions.ADDVULNWEBSRV: model.api.addVulnWebToService,
            #Note
            Modelactions.ADDNOTEHOST: model.api.addNoteToHost,
            Modelactions.ADDNOTESRV: model.api.addNoteToService,
            Modelactions.ADDNOTENOTE: model.api.addNoteToNote,
            #Creds
            Modelactions.ADDCREDSRV:  model.api.addCredToService,
            #LOG
            Modelactions.LOG: model.api.log,
            Modelactions.DEVLOG: model.api.devlog,
            # Plugin state
            Modelactions.PLUGINSTART: model.api.pluginStart,
            Modelactions.PLUGINEND: model.api.pluginEnd
        }

    def updatePluginSettings(self, plugin_id, new_settings):
        for plugin_set in self.plugin_sets.values():
            if plugin_id in plugin_set:
                plugin_set[plugin_id].updateSettings(new_settings)
        if plugin_id in self._plugins:
            self._plugins[plugin_id].updateSettings(new_settings)

    def createPluginSet(self, pid):
        self.plugin_sets[pid] = self.plugin_manager.getPlugins()

    def processCommandInput(self, pid, cmd, pwd):
        """
        This method tries to find a plugin to parse the command sent
        by the terminal (identiefied by the process id).
        """
        if pid not in self.plugin_sets:
            self.createPluginSet(pid)

        plugin = self._get_plugins_by_input(cmd, self.plugin_sets[pid])

        if plugin:
            modified_cmd_string = plugin.processCommandString("", pwd, cmd)
            if not self._is_command_malformed(cmd, modified_cmd_string):

                cmd_info = CommandRunInformation(
                    **{'workspace': model.api.getActiveWorkspace().name,
                        'itime': time.time(),
                        'import_source': 'shell',
                        'command': cmd.split()[0],
                        'params': ' '.join(cmd.split()[1:])})
                cmd_info.setID(self._mapper_manager.save(cmd_info))

                self._active_plugins[pid] = plugin, cmd_info

                return plugin.id, modified_cmd_string

        return None, None

    def onCommandFinished(self, pid, exit_code, term_output):

        if pid not in self._active_plugins.keys():
            return False
        if exit_code != 0:
            del self._active_plugins[pid]
            return False

        plugin, cmd_info = self._active_plugins.get(pid)

        cmd_info.duration = time.time() - cmd_info.itime
        self._mapper_manager.update(cmd_info)

        self.processOutput(plugin, term_output, cmd_info)
        del self._active_plugins[pid]
        return True

    def processReport(self, plugin, filepath, ws_name=None):
        if not ws_name:
            ws_name = model.api.getActiveWorkspace().name
        cmd_info = CommandRunInformation(
            **{'workspace': ws_name,
                'itime': time.time(),
                'import_source': 'report',
                'command': plugin,
                'params': filepath,
            })
        self._mapper_manager.createMappers(ws_name)
        command_id = self._mapper_manager.save(cmd_info)
        cmd_info.setID(command_id)
        if plugin in self._plugins:
            logger.info('Processing report with plugin {0}'.format(plugin))
            self._plugins[plugin].workspace = ws_name
            with open(filepath, 'rb') as output:
                self.processOutput(self._plugins[plugin], output.read(), cmd_info, True)
            return command_id
        return False

    def clearActivePlugins(self):
        self._active_plugins = {}