Beispiel #1
0
class LoginDialogBase(gui_utilities.GladeGObject):
    """
	This object is basic login dialog object that can be inherited from and
	customized.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('button_connect', 'entry_server', 'entry_username',
                  'entry_password', 'label_main'),
        name='LoginDialogBase')
    label = None
    top_gobject = 'dialog'

    def __init__(self, *args, **kwargs):
        super(LoginDialogBase, self).__init__(*args, **kwargs)
        if self.label is not None:
            self.gobjects['label_main'].set_text(self.label)

    def interact(self):
        self.dialog.show_all()
        response = self.dialog.run()
        if response != Gtk.ResponseType.CANCEL:
            self.objects_save_to_config()
        self.dialog.destroy()
        return response

    def signal_entry_activate(self, entry):
        self.gobjects['button_connect'].emit('clicked')
Beispiel #2
0
class LoginDialog(LoginDialogBase):
    """
	This object is the main King Phisher login dialog, it is used to
	prompt for connection information for the King Phisher server.

	It allows the user to specify the host and port to connect to and
	credentials for authentication.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('button_connect', 'entry_server', 'entry_server_username',
                  'entry_server_password', 'entry_server_one_time_password',
                  'label_main', 'label_server_one_time_password',
                  'revealer_server_one_time_password',
                  'spinbutton_server_remote_port', 'switch_server_use_ssl'),
        top_level=('PortAdjustment', ))

    def __init__(self, *args, **kwargs):
        super(LoginDialog, self).__init__(*args, **kwargs)
        self.popup_menu = managers.MenuManager()
        self.popup_menu.append(
            'About', lambda x: about.AboutDialog(self.application).interact())
        self.popup_menu.append('Import Configuration',
                               self.signal_menuitem_activate_import_config)

    def signal_button_pressed(self, _, event):
        if not (event.type == Gdk.EventType.BUTTON_PRESS
                and event.button == Gdk.BUTTON_SECONDARY):
            return
        self.popup_menu.menu.popup(
            None, None,
            functools.partial(gui_utilities.gtk_menu_position, event), None,
            event.button, event.time)
        return True

    def signal_menuitem_activate_import_config(self, _):
        dialog = extras.FileChooserDialog('Import Configuration File',
                                          self.dialog)
        response = dialog.run_quick_open()
        dialog.destroy()
        if response is None:
            return
        config_path = response['target_path']
        try:
            self.application.merge_config(config_path, strict=False)
        except Exception:
            self.logger.warning('failed to merge configuration file: ' +
                                config_path,
                                exc_info=True)
            gui_utilities.show_dialog_error(
                'Invalid Configuration File', self.dialog,
                'Could not import the configuration file.')
        else:
            self.objects_load_from_config()

    def signal_switch_ssl(self, switch, _):
        if switch.get_property('active'):
            self.gobjects['spinbutton_server_remote_port'].set_value(443)
        else:
            self.gobjects['spinbutton_server_remote_port'].set_value(80)
class RPCTerminalAppWindow(gui_utilities.GladeGObject):
    dependencies = gui_utilities.GladeDependencies(
        children=('box_main', 'menu_edit', 'menu_help'),
        top_level=('StockDialogQuestionImage', 'StockHelpImage'))
    top_gobject = 'window'

    def __init__(self, terminal, *args, **kwargs):
        super(RPCTerminalAppWindow, self).__init__(*args, **kwargs)
        self.terminal = terminal
        self.child_pid = None
        self.gobjects['box_main'].pack_end(self.terminal, True, True, 0)
        if hasattr(self.terminal.props, 'rewrap_on_resize'):
            self.terminal.set_property('rewrap-on-resize', True)
        self.terminal.set_property('scroll-on-keystroke', True)
        self.terminal.set_property('scrollback-lines', 2048)

    def signal_menuitem_edit_copy(self, menuitem):
        self.terminal.copy_clipboard()

    def signal_menuitem_edit_paste(self, menuitem):
        self.terminal.paste_clipboard()

    def signal_menuitem_help_about(self, menuitem):
        dialogs.AboutDialog(self.application).interact()

    def signal_menuitem_help_api_docs(self, menuitem):
        rpc_api_docs_url = "https://king-phisher.readthedocs.io/en/{0}/server/rpc_api.html".format(
            'latest' if version.version_label in ('alpha',
                                                  'beta') else 'stable')
        utilities.open_uri(rpc_api_docs_url)

    def signal_menuitem_help_wiki(self, menuitem):
        utilities.open_uri('https://github.com/securestate/king-phisher/wiki')

    def signal_menuitem_view_zoom_in(self, menuitem):
        font_scale = self.terminal.get_property('font-scale')
        font_scale += font_scale * ZOOM_RATE
        self.terminal.set_property('font-scale', font_scale)

    def signal_menuitem_view_zoom_out(self, menuitem):
        font_scale = self.terminal.get_property('font-scale')
        font_scale = font_scale / (1.0 + ZOOM_RATE)
        self.terminal.set_property('font-scale', font_scale)

    def signal_menuitem_view_zoom_reset(self, menuitem):
        self.terminal.set_property('font-scale', 1.0)

    def signal_window_destroy(self, window):
        if self.child_pid is None:
            self.logger.error(
                'signal_window_destory was called but the child pid is None')
            return
        if os.path.exists("/proc/{0}".format(self.child_pid)):
            self.logger.debug("sending sigkill to child process: {0}".format(
                self.child_pid))
            os.kill(self.child_pid, signal.SIGKILL)
Beispiel #4
0
class BaseHostKeyDialog(gui_utilities.GladeGObject):
	"""
	A base class for dialogs which show information about SSH host keys. It is
	assumed that the widgets defined in :py:attr:`.dependencies` are present
	including one button to accept the host key, and one to reject. The class's
	default response can be set using :py:attr:`.default_response`.
	"""
	dependencies = gui_utilities.GladeDependencies(
		children=(
			'button_accept',
			'button_reject',
			'textview_key_details'
		),
		top_level=(
			'StockApplyImage',
			'StockStopImage'
		)
	)
	top_gobject = 'dialog'
	default_response = None
	"""The response that should be selected as the default for the dialog."""
	def __init__(self, application, hostname, key):
		"""
		:param application: The application to associate this popup dialog with.
		:type application: :py:class:`.KingPhisherClientApplication`
		:param str hostname: The hostname associated with the key.
		:param key: The host's SSH key.
		:type key: :py:class:`paramiko.pkey.PKey`
		"""
		super(BaseHostKeyDialog, self).__init__(application)
		self.hostname = hostname
		self.key = key
		textview = self.gobjects['textview_key_details']
		textview.modify_font(Pango.FontDescription('monospace 9'))
		textview.get_buffer().set_text(self.key_details)
		if self.default_response is not None:
			button = self.dialog.get_widget_for_response(response_id=self.default_response)
			button.grab_default()

	@property
	def key_details(self):
		key_type = self.key.get_name().lower()
		details = "Host: {0} ({1})\n".format(self.hostname, key_type)
		if key_type.startswith('ssh-'):
			key_type = key_type[4:]
		key_type = key_type.split('-', 1)[0].upper()
		details += "{0} key fingerprint is SHA256:{1}.\n".format(key_type, base64.b64encode(hashlib.new('sha256', self.key.asbytes()).digest()).decode('utf-8'))
		details += "{0} key fingerprint is MD5:{1}.\n".format(key_type, binascii.b2a_hex(hashlib.new('md5', self.key.asbytes()).digest()).decode('utf-8'))
		return details

	def interact(self):
		self.dialog.show_all()
		response = self.dialog.run()
		self.dialog.destroy()
		return response
Beispiel #5
0
class ExceptionDialog(gui_utilities.GladeGObject):
    """
	Display a dialog which shows an error message for a python exception.
	The dialog includes useful details for reporting and debugging the exception
	which occurred.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('linkbutton_github_issues', ))
    top_gobject = 'dialog'

    def __init__(self, application, exc_info=None, error_uid=None):
        """
		:param application: The parent application for this object.
		:type application: :py:class:`Gtk.Application`
		:param tuple exc_info: The exception information as provided by :py:func:`sys.exc_info`.
		:param str error_uid: An optional unique identifier for the exception that can be provided for tracking purposes.
		"""
        super(ExceptionDialog, self).__init__(application)
        self.error_description = self.gtk_builder_get(
            'label_error_description')
        self.error_details = self.gtk_builder_get('textview_error_details')
        self.error_details.modify_font(Pango.FontDescription('monospace 9'))
        self.exc_info = exc_info or sys.exc_info()
        self.error_uid = error_uid
        linkbutton = self.gobjects['linkbutton_github_issues']
        linkbutton.set_label('Project Issue Tracker')
        linkbutton.connect(
            'activate-link',
            lambda _: utilities.open_uri(linkbutton.get_property('uri')))

    def interact(self):
        exc_type, exc_value, exc_traceback = self.exc_info
        details = format_exception_details(exc_type, exc_value, exc_traceback,
                                           self.error_uid)

        self.error_description.set_text("Error type: {0}".format(
            format_exception_name(exc_type)))
        self.error_details.get_buffer().set_text(details)

        self.dialog.show_all()
        self.dialog.run()
        self.dialog.destroy()
        return

    @classmethod
    def _interact_on_idle(cls, args, kwargs):
        instance = cls(*args, **kwargs)
        instance.interact()

    @classmethod
    def interact_on_idle(cls, *args, **kwargs):
        GLib.idle_add(cls._interact_on_idle, args, kwargs)
Beispiel #6
0
class HTMLWindow(gui_utilities.GladeGObject):
	"""
	This basic window contains a :py:class:`~.extras.WebKitHTMLView` widget for
	rendering and displaying HTML content.
	"""
	dependencies = gui_utilities.GladeDependencies(name='HTMLWindow')
	top_gobject = 'window'
	def __init__(self, application):
		super(HTMLWindow, self).__init__(application)
		self.webview = extras.WebKitHTMLView()
		"""The :py:class:`~.extras.WebKitHTMLView` widget instance."""
		self.webview.show()
		self.window.add(self.webview)
Beispiel #7
0
class _TimeSelector(gui_utilities.GladeGObject):
    """
	This is the TimeSelector :py:class:`~Gtk.Popover` object containing the
	:py:class:`~Gtk.SpinButton` widgets. This class should be treated as
	private, as it is created by the :py:class:`~TimeSelectorButtonManager`
	class. It should not be used directly.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('spinbutton_hour', 'spinbutton_minute'),
        top_level=('ClockHourAdjustment', 'ClockMinuteAdjustment'),
        name='TimeSelector')
    top_gobject = 'popover'

    def signal_spinbutton_output(self, spinbutton):
        adjustment = spinbutton.get_adjustment()
        value = adjustment.get_value()
        spinbutton.set_text("{0:02.0f}".format(value))
        return True
Beispiel #8
0
class LoginDialog(LoginDialogBase):
    """
	This object is the main King Phisher login dialog, it is used to
	prompt for connection information for the King Phisher server.

	It allows the user to specify the host and port to connect to and
	credentials for authentication.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('button_connect', 'entry_server', 'entry_server_username',
                  'entry_server_password', 'entry_server_one_time_password',
                  'label_main', 'label_server_one_time_password',
                  'revealer_server_one_time_password',
                  'spinbutton_server_remote_port', 'switch_server_use_ssl'),
        top_level=('PortAdjustment', ))

    def __init__(self, *args, **kwargs):
        super(LoginDialog, self).__init__(*args, **kwargs)
        self.popup_menu = Gtk.Menu.new()
        menu_item = Gtk.MenuItem.new_with_label('About')
        menu_item.connect(
            'activate',
            lambda x: about.AboutDialog(self.application).interact())
        self.popup_menu.append(menu_item)
        self.popup_menu.show_all()

    def signal_switch_ssl(self, switch, _):
        if switch.get_property('active'):
            self.gobjects['spinbutton_server_remote_port'].set_value(443)
        else:
            self.gobjects['spinbutton_server_remote_port'].set_value(80)

    def signal_button_pressed(self, widget, event):
        if not (event.type == Gdk.EventType.BUTTON_PRESS
                and event.button == 3):
            return
        self.popup_menu.popup(
            None, None,
            functools.partial(gui_utilities.gtk_menu_position, event), None,
            event.button, event.time)
        return True
Beispiel #9
0
class TagEditorDialog(gui_utilities.GladeGObject):
    """
	Display a dialog which can be used to edit the various tags that are
	present on the remote server. This can be used to rename tags and modify
	their descriptions.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('notebook', 'button_close', 'treeview_campaign_types',
                  'treeview_company_departments', 'treeview_industries'))
    top_gobject = 'dialog'
    objects_persist = False
    tag_tables = ('campaign_types', 'company_departments', 'industries')

    def __init__(self, *args, **kwargs):
        super(TagEditorDialog, self).__init__(*args, **kwargs)
        self.popup_menus = {}
        self.treeview_managers = {}
        for tag_table in self.tag_tables:
            treeview = self.gobjects['treeview_' + tag_table]
            model = Gtk.ListStore(int, str, str)
            treeview.set_model(model)
            tvm = widget_managers.TreeViewManager(
                treeview,
                cb_delete=functools.partial(self.delete_tag, tag_table),
                cb_refresh=functools.partial(self.load_tags, tag_table))
            name_renderer = Gtk.CellRendererText()
            name_renderer.connect('edited', self.signal_renderer_edited,
                                  (tag_table, 1, 'name'))
            name_renderer.set_property('editable', True)
            description_renderer = Gtk.CellRendererText()
            description_renderer.connect('edited', self.signal_renderer_edited,
                                         (tag_table, 2, 'description'))
            description_renderer.set_property('editable', True)
            description_renderer.set_property('ellipsize',
                                              Pango.EllipsizeMode.END)
            tvm.set_column_titles(
                ('Name', 'Description'),
                column_offset=1,
                renderers=(name_renderer, description_renderer))
            self.treeview_managers[tag_table] = tvm
            self.popup_menus[tag_table] = tvm.get_popup_menu()
        self.load_tags()

    def delete_tag(self, tag_table, treeview, selection):
        (model, tree_iter) = selection.get_selected()
        if not tree_iter:
            return
        tag_id = model.get_value(tree_iter, 0)
        if not gui_utilities.show_dialog_yes_no(
                'Delete This Tag?', self.dialog,
                'This action is irreversible.'):
            return
        self.application.rpc('db/table/delete', tag_table, tag_id)
        self.load_tags(tag_table)

    def load_tags(self, tags=None):
        if tags is None:
            tags = self.tag_tables
        elif isinstance(tags, str):
            tags = (tags, )
        for tag in tags:
            model = self.gobjects['treeview_' + tag].get_model()
            model.clear()
            for tag in self.application.rpc.remote_table(tag):
                model.append((tag.id, tag.name, tag.description))

    def interact(self):
        self.dialog.show_all()
        self.dialog.run()
        self.dialog.destroy()

    def signal_renderer_edited(self, cell, path, property_value, details):
        tag_table, store_id, property_name = details
        model = self.gobjects['treeview_' + tag_table].get_model()
        model_iter = model.get_iter(path)
        tag_id = model.get_value(model_iter, 0)

        try:
            self.application.rpc('db/table/set', tag_table, tag_id,
                                 (property_name, ), (property_value, ))
        except AdvancedHTTPServer.AdvancedHTTPServerRPCError:
            gui_utilities.show_dialog_error(
                'Failed To Modify', self.dialog,
                'An error occurred while modifying the information.')
        else:
            model.set_value(model_iter, store_id, property_value)
class ConfigurationDialog(gui_utilities.GladeGObject):
    """
	Display the King Phisher client configuration dialog. Running this dialog
	via the :py:meth:`.interact` method will cause some server settings to be
	loaded.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=(
            # Server Tab
            'entry_server',
            'entry_server_username',
            'entry_sms_phone_number',
            'combobox_sms_carrier',
            # SMTP Server Tab
            'entry_smtp_server',
            'entry_smtp_username',
            'spinbutton_smtp_max_send_rate',
            'switch_smtp_ssl_enable',
            'switch_smtp_ssh_enable',
            'entry_sftp_client',
            'entry_ssh_server',
            'entry_ssh_username',
            # Client Tab
            'checkbutton_remove_attachment_metadata',
            'combobox_spf_check_level'),
        top_level=('SMSCarriers', 'SMTPSendRate', 'SPFCheckLevels'))
    top_gobject = 'dialog'

    def __init__(self, *args, **kwargs):
        super(ConfigurationDialog, self).__init__(*args, **kwargs)
        self.gobjects['entry_smtp_server'].set_sensitive(
            not self.gobjects['switch_smtp_ssh_enable'].get_active())

    def signal_switch_smtp_ssh(self, switch, _):
        active = switch.get_property('active')
        entry = self.gobjects['entry_smtp_server']
        self.gtk_builder_get('frame_smtp_ssh').set_sensitive(active)
        if active:
            entry.set_sensitive(False)
            entry.set_text('localhost:25')
        else:
            entry.set_sensitive(True)

    def signal_toggle_alert_subscribe(self, cbutton):
        active = cbutton.get_property('active')
        if active:
            remote_method = 'campaign/alerts/subscribe'
        else:
            remote_method = 'campaign/alerts/unsubscribe'
        self.application.rpc(remote_method, self.config['campaign_id'])

    def signal_toggle_reject_after_credentials(self, cbutton):
        self.application.rpc('db/table/set', 'campaigns',
                             self.config['campaign_id'],
                             'reject_after_credentials',
                             cbutton.get_property('active'))

    def signal_changed_spf_check_level(self, combobox):
        ti = combobox.get_active_iter()
        if not ti:
            return
        model = combobox.get_model()
        label = self.gtk_builder_get('label_spf_level_description')
        level_description = model[ti][2]
        label.set_text(level_description)

    def _configure_settings_dashboard(self):
        if not graphs.has_matplotlib:
            self.gtk_builder_get('frame_dashboard').set_sensitive(False)
            return
        graph_providers = Gtk.ListStore(str, str)
        for graph in graphs.get_graphs():
            graph = graphs.get_graph(graph)
            graph_providers.append([graph.name_human, graph.name])
        for dash_position in ['top_left', 'top_right', 'bottom']:
            combobox = self.gtk_builder_get('combobox_dashboard_' +
                                            dash_position)
            combobox.set_model(graph_providers)
            ti = gui_utilities.gtk_list_store_search(
                graph_providers,
                self.config.get('dashboard.' + dash_position),
                column=1)
            combobox.set_active_iter(ti)

    def _configure_settings_server(self):
        cb_subscribed = self.gtk_builder_get('checkbutton_alert_subscribe')
        cb_reject_after_creds = self.gtk_builder_get(
            'checkbutton_reject_after_credentials')
        entry_beef_hook = self.gtk_builder_get('entry_server_beef_hook')
        server_config = self.application.rpc(
            'config/get',
            ['beef.hook_url', 'server.require_id', 'server.secret_id'])
        entry_beef_hook.set_property('text',
                                     server_config.get('beef.hook_url', ''))
        self.config['server_config']['server.require_id'] = server_config[
            'server.require_id']
        self.config['server_config']['server.secret_id'] = server_config[
            'server.secret_id']
        # older versions of GObject.signal_handler_find seem to have a bug which cause a segmentation fault in python
        if GObject.pygobject_version < (3, 10):
            cb_subscribed.set_property(
                'active',
                self.application.rpc('campaign/alerts/is_subscribed',
                                     self.config['campaign_id']))
            cb_reject_after_creds.set_property(
                'active',
                self.application.rpc.remote_table_row(
                    'campaigns',
                    self.config['campaign_id']).reject_after_credentials)
        else:
            with gui_utilities.gobject_signal_blocked(cb_subscribed,
                                                      'toggled'):
                cb_subscribed.set_property(
                    'active',
                    self.application.rpc('campaign/alerts/is_subscribed',
                                         self.config['campaign_id']))
                cb_reject_after_creds.set_property(
                    'active',
                    self.application.rpc.remote_table_row(
                        'campaigns',
                        self.config['campaign_id']).reject_after_credentials)
        cb_reject_after_creds.set_sensitive(
            self.config['server_config']['server.require_id'])

    def _finialize_settings_dashboard(self):
        dashboard_changed = False
        for dash_position in ['top_left', 'top_right', 'bottom']:
            combobox = self.gtk_builder_get('combobox_dashboard_' +
                                            dash_position)
            ti = combobox.get_active_iter()
            if not ti:
                continue
            graph_providers = combobox.get_model()
            graph_name = graph_providers[ti][1]
            if self.config.get('dashboard.' + dash_position) == graph_name:
                continue
            self.config['dashboard.' + dash_position] = graph_name
            dashboard_changed = True
        if dashboard_changed:
            gui_utilities.show_dialog_info(
                'The dashboard layout has been updated.', self.parent,
                'The new settings will be applied the next time the application starts.'
            )

    def interact(self):
        self._configure_settings_dashboard()
        self._configure_settings_server()
        self.gtk_builder_get('combobox_spf_check_level').emit('changed')

        self.dialog.show_all()
        response = self.dialog.run()
        if response != Gtk.ResponseType.CANCEL:
            self.objects_save_to_config()
            self.verify_sms_settings()
            entry_beef_hook = self.gtk_builder_get('entry_server_beef_hook')
            self.application.rpc('config/set', {
                'beef.hook_url':
                entry_beef_hook.get_property('text').strip()
            })
            if graphs.has_matplotlib:
                self._finialize_settings_dashboard()
        self.dialog.destroy()
        return response

    def verify_sms_settings(self):
        phone_number = gui_utilities.gobject_get_value(
            self.gobjects['entry_sms_phone_number'])
        phone_number_set = bool(phone_number)
        sms_carrier_set = bool(
            self.gobjects['combobox_sms_carrier'].get_active() > 0)
        if phone_number_set ^ sms_carrier_set:
            gui_utilities.show_dialog_warning(
                'Missing Information', self.parent,
                'Both a phone number and a valid carrier must be specified')
            if 'sms_phone_number' in self.config:
                del self.config['sms_phone_number']
            if 'sms_carrier' in self.config:
                del self.config['sms_carrier']
        elif phone_number_set and sms_carrier_set:
            phone_number = ''.join(d for d in phone_number
                                   if d in string.digits)
            if len(phone_number) != 10:
                gui_utilities.show_dialog_warning(
                    'Invalid Phone Number', self.parent,
                    'The phone number must contain exactly 10 digits')
                return
            username = self.config['server_username']
            self.application.rpc('db/table/set', 'users', username,
                                 ('phone_number', 'phone_carrier'),
                                 (phone_number, self.config['sms_carrier']))
Beispiel #11
0
class CampaignAssistant(gui_utilities.GladeGObject):
	"""
	Display an assistant which walks the user through creating a new campaign or
	configuring an existing campaign. If no *campaign_id* is specified a new
	campaign will be created.
	"""
	dependencies = gui_utilities.GladeDependencies(
		children=(
			resources.CompanyEditorGrid(
				gui_utilities.GladeProxyDestination(
					widget='alignment_company',
					method='add'
				)
			),
			'calendar_campaign_expiration',
			'checkbutton_alert_subscribe',
			'checkbutton_expire_campaign',
			'checkbutton_reject_after_credentials',
			'combobox_campaign_type',
			'combobox_company_existing',
			'entry_campaign_name',
			'entry_campaign_description',
			'entry_test_validation_text',
			'entry_validation_regex_username',
			'entry_validation_regex_password',
			'entry_validation_regex_mfa_token',
			'frame_campaign_expiration',
			'frame_company_existing',
			'frame_company_new',
			'image_intro_title',
			'label_confirm_body',
			'label_confirm_title',
			'label_intro_body',
			'label_intro_title',
			'label_validation_regex_username',
			'label_validation_regex_password',
			'label_validation_regex_mfa_token',
			'radiobutton_company_existing',
			'radiobutton_company_new',
			'radiobutton_company_none',
			'togglebutton_expiration_time'
		),
		top_level=(
			'ClockHourAdjustment',
			'ClockMinuteAdjustment'
		)
	)
	top_gobject = 'assistant'
	objects_persist = False
	def __init__(self, application, campaign_id=None):
		"""
		:param application: The application instance which this object belongs to.
		:type application: :py:class:`~king_phisher.client.application.KingPhisherClientApplication`
		:param campaign_id: The ID of the campaign to edit.
		"""
		super(CampaignAssistant, self).__init__(application)
		self.campaign_id = campaign_id
		self._close_ready = True
		self._page_titles = {}
		for page_n in range(self.assistant.get_n_pages()):
			page = self.assistant.get_nth_page(page_n)
			page_title = self.assistant.get_page_title(page)
			if page_title:
				self._page_titles[page_title] = page_n

		self._expiration_time = managers.TimeSelectorButtonManager(self.application, self.gobjects['togglebutton_expiration_time'])
		self._set_comboboxes()
		self._set_defaults()

		if not self.config['server_config']['server.require_id']:
			self.gobjects['checkbutton_reject_after_credentials'].set_sensitive(False)
			self.gobjects['checkbutton_reject_after_credentials'].set_property('active', False)

		confirm_preamble = 'Verify all settings are correct in the previous sections'
		if campaign_id:
			# re-configuring an existing campaign
			self.gobjects['label_confirm_body'].set_text(confirm_preamble + ', then hit "Apply" to update the King Phisher campaign with the new settings.')
			self.gobjects['label_intro_body'].set_text('This assistant will walk you through reconfiguring the selected King Phisher campaign.')
			self.gobjects['label_intro_title'].set_text('Configure Campaign')
		else:
			# creating a new campaign
			self.gobjects['label_confirm_body'].set_text(confirm_preamble + ', then hit "Apply" to create the new King Phisher campaign.')
			self.gobjects['label_intro_body'].set_text('This assistant will walk you through creating and configuring a new King Phisher campaign.')
			self.gobjects['label_intro_title'].set_text('New Campaign')

	@property
	def campaign_name(self):
		"""
		The string value of the configured campaign name. This may be set even
		when the campaign was not created, which would be the case if the user
		closed the window.
		"""
		return self.gobjects['entry_campaign_name'].get_text()

	@property
	def is_editing_campaign(self):
		return self.campaign_id is not None

	@property
	def is_new_campaign(self):
		return self.campaign_id is None

	def _set_comboboxes(self):
		"""Set up all the comboboxes and load the data for their models."""
		renderer = resources.renderer_text_desc
		rpc = self.application.rpc
		for tag_name, tag_table in (('campaign_type', 'campaign_types'), ('company_existing', 'companies'), ('company_industry', 'industries')):
			combobox = self.gobjects['combobox_' + tag_name]
			model = combobox.get_model()
			if model is None:
				combobox.pack_start(renderer, True)
				combobox.add_attribute(renderer, 'text', 2)
			combobox.set_model(rpc.get_tag_model(tag_table, model=model))

	def _set_defaults(self):
		"""
		Set any default values for widgets. Also load settings from the existing
		campaign if one was specified.
		"""
		calendar = self.gobjects['calendar_campaign_expiration']
		default_day = datetime.datetime.today() + datetime.timedelta(days=31)
		gui_utilities.gtk_calendar_set_pydate(calendar, default_day)

		if self.campaign_id is None:
			return
		campaign = self.application.get_graphql_campaign()

		# set entries
		self.gobjects['entry_campaign_name'].set_text(campaign['name'])
		self.gobjects['entry_validation_regex_username'].set_text(campaign['credentialRegexUsername'] or '')
		self.gobjects['entry_validation_regex_password'].set_text(campaign['credentialRegexPassword'] or '')
		self.gobjects['entry_validation_regex_mfa_token'].set_text(campaign['credentialRegexMfaToken'] or '')

		if campaign['description'] is not None:
			self.gobjects['entry_campaign_description'].set_text(campaign['description'])
		if campaign['campaignType'] is not None:
			combobox = self.gobjects['combobox_campaign_type']
			model = combobox.get_model()
			model_iter = gui_utilities.gtk_list_store_search(model, campaign['campaignType']['id'], column=0)
			if model_iter is not None:
				combobox.set_active_iter(model_iter)

		self.gobjects['checkbutton_alert_subscribe'].set_property('active', self.application.rpc('campaign/alerts/is_subscribed', self.campaign_id))
		self.gobjects['checkbutton_reject_after_credentials'].set_property('active', bool(campaign['maxCredentials']))

		if campaign['company'] is not None:
			self.gobjects['radiobutton_company_existing'].set_active(True)
			combobox = self.gobjects['combobox_company_existing']
			model = combobox.get_model()
			model_iter = gui_utilities.gtk_list_store_search(model, campaign['company']['id'], column=0)
			if model_iter is not None:
				combobox.set_active_iter(model_iter)

		if campaign['expiration'] is not None:
			expiration = utilities.datetime_utc_to_local(campaign['expiration'])
			self.gobjects['checkbutton_expire_campaign'].set_active(True)
			self._expiration_time.time = expiration.time()
			gui_utilities.gtk_calendar_set_pydate(self.gobjects['calendar_campaign_expiration'], expiration.date())

	def _get_tag_from_combobox(self, combobox, db_table):
		model = combobox.get_model()
		model_iter = combobox.get_active_iter()
		if model_iter is not None:
			return model.get_value(model_iter, 0)
		campaign_type = combobox.get_child().get_text().strip()
		if not campaign_type:
			return
		model_iter = gui_utilities.gtk_list_store_search(model, campaign_type, column=1)
		if model_iter is None:
			return self.application.rpc('db/table/insert', db_table, 'name', campaign_type)
		return model.get_value(model_iter, 0)

	def _get_company_existing_id(self):
		combobox_company = self.gobjects['combobox_company_existing']
		model = combobox_company.get_model()
		model_iter = combobox_company.get_active_iter()
		if model is None or model_iter is None:
			return
		return model.get_value(model_iter, 0)

	def _get_company_new_id(self):
		name = self.gobjects['entry_company_name'].get_text()
		name = name.strip()
		# check if this company name already exists in the model
		model = self.gobjects['combobox_company_existing'].get_model()
		model_iter = gui_utilities.gtk_list_store_search(model, name, column=1)
		if model_iter is not None:
			return model.get_value(model_iter, 0)
		# check if this company name already exists remotely
		remote_company = self._get_graphql_company(name)
		if remote_company:
			return remote_company['id']

		company_id = self.application.rpc(
			'db/table/insert',
			'companies',
			('name', 'description', 'industry_id', 'url_main', 'url_email', 'url_remote_access'),
			(
				name,
				self.get_entry_value('company_description'),
				self._get_tag_from_combobox(self.gobjects['combobox_company_industry'], 'industries'),
				self.get_entry_value('company_url_main'),
				self.get_entry_value('company_url_email'),
				self.get_entry_value('company_url_remote_access')
			)
		)
		self.gobjects['radiobutton_company_existing'].set_active(True)
		return company_id

	def _get_graphql_company(self, company_name):
		results = self.application.rpc.graphql("""\
		query getCompany($name: String!) {
			db {
				company(name: $name) {
					id
				}
			}
		}""", {'name': company_name})
		return results['db']['company']

	def _do_regex_validation(self, test_text, entry):
		try:
			regex = re.compile(entry.get_text())
		except re.error:
			entry.set_property('secondary-icon-stock', 'gtk-dialog-warning')
			return
		result = True
		if regex.pattern and test_text:
			result = regex.match(test_text) is not None
		entry.set_property('secondary-icon-stock', 'gtk-yes' if result else 'gtk-no')

	def signal_assistant_apply(self, _):
		self._close_ready = False
		# have to do it this way because the next page will be selected when the apply signal is complete
		set_current_page = lambda page_name: self.assistant.set_current_page(max(0, self._page_titles[page_name] - 1))

		# get and validate the campaign name
		campaign_name = self.gobjects['entry_campaign_name'].get_text()
		campaign_name = campaign_name.strip()
		if not campaign_name:
			gui_utilities.show_dialog_error('Invalid Campaign Name', self.parent, 'A unique and valid campaign name must be specified.')
			set_current_page('Basic Settings')
			return True

		properties = {}

		# validate the credential validation regular expressions
		for field in ('username', 'password', 'mfa_token'):
			regex = self.gobjects['entry_validation_regex_' + field].get_text()
			if regex:
				try:
					re.compile(regex)
				except re.error:
					label = self.gobjects['label_validation_regex_' + field].get_text()
					gui_utilities.show_dialog_error('Invalid Regex', self.parent, "The '{0}' regular expression is invalid.".format(label))
					return True
			else:
				regex = None  # keep empty strings out of the database
			properties['credential_regex_' + field] = regex

		# validate the company
		company_id = None
		if self.gobjects['radiobutton_company_existing'].get_active():
			company_id = self._get_company_existing_id()
			if company_id is None:
				gui_utilities.show_dialog_error('Invalid Company', self.parent, 'A valid existing company must be specified.')
				set_current_page('Company')
				return True
		elif self.gobjects['radiobutton_company_new'].get_active():
			company_id = self._get_company_new_id()
			if company_id is None:
				gui_utilities.show_dialog_error('Invalid Company', self.parent, 'The new company settings are invalid.')
				set_current_page('Company')
				return True

		# get and validate the campaign expiration
		expiration = None
		if self.gobjects['checkbutton_expire_campaign'].get_property('active'):
			expiration = datetime.datetime.combine(
				gui_utilities.gtk_calendar_get_pydate(self.gobjects['calendar_campaign_expiration']),
				self._expiration_time.time
			)
			expiration = utilities.datetime_local_to_utc(expiration)
			if self.is_new_campaign and expiration <= datetime.datetime.now():
				gui_utilities.show_dialog_error('Invalid Campaign Expiration', self.parent, 'The expiration date is set in the past.')
				set_current_page('Expiration')
				return True

		# point of no return
		campaign_description = self.get_entry_value('campaign_description')
		if self.campaign_id:
			properties['name'] = self.campaign_name
			properties['description'] = campaign_description
			cid = self.campaign_id
		else:
			try:
				cid = self.application.rpc('campaign/new', campaign_name, description=campaign_description)
			except advancedhttpserver.RPCError as error:
				if not error.is_remote_exception:
					raise error
				if not error.remote_exception['name'] == 'exceptions.ValueError':
					raise error
				error_message = error.remote_exception.get('message', 'an unknown error occurred').capitalize() + '.'
				gui_utilities.show_dialog_error('Failed To Create Campaign', self.parent, error_message)
				set_current_page('Basic Settings')
				return True
			self.application.emit('campaign-created', cid)

		properties['campaign_type_id'] = self._get_tag_from_combobox(self.gobjects['combobox_campaign_type'], 'campaign_types')
		properties['company_id'] = company_id
		properties['expiration'] = expiration
		properties['max_credentials'] = (1 if self.gobjects['checkbutton_reject_after_credentials'].get_property('active') else None)
		self.application.rpc('db/table/set', 'campaigns', cid, tuple(properties.keys()), tuple(properties.values()))

		should_subscribe = self.gobjects['checkbutton_alert_subscribe'].get_property('active')
		if should_subscribe != self.application.rpc('campaign/alerts/is_subscribed', cid):
			if should_subscribe:
				self.application.rpc('campaign/alerts/subscribe', cid)
			else:
				self.application.rpc('campaign/alerts/unsubscribe', cid)

		self.application.emit('campaign-changed', cid)
		self._close_ready = True
		return

	def signal_assistant_cancel(self, assistant):
		assistant.destroy()

	def signal_assistant_close(self, assistant):
		if self._close_ready:
			assistant.destroy()
		self._close_ready = True

	def signal_assistant_prepare(self, _, page):
		page_title = self.assistant.get_page_title(page)
		if page_title == 'Company':
			combobox = self.gobjects['combobox_company_existing']
			model = combobox.get_model()
			company_name = self.get_entry_value('company_name')
			if company_name:
				model_iter = gui_utilities.gtk_list_store_search(model, company_name, column=1)
				if model_iter is not None:
					combobox.set_active_iter(model_iter)
					self.gobjects['radiobutton_company_existing'].set_active(True)

	def signal_calendar_prev(self, calendar):
		today = datetime.date.today()
		calendar_day = gui_utilities.gtk_calendar_get_pydate(calendar)
		if calendar_day >= today:
			return
		gui_utilities.gtk_calendar_set_pydate(calendar, today)

	def signal_checkbutton_expire_campaign_toggled(self, _):
		active = self.gobjects['checkbutton_expire_campaign'].get_property('active')
		self.gobjects['frame_campaign_expiration'].set_sensitive(active)

	def signal_entry_changed_test_validation_text(self, field):
		test_text = field.get_text()
		for field in ('username', 'password', 'mfa_token'):
			self._do_regex_validation(test_text, self.gobjects['entry_validation_regex_' + field])

	def signal_entry_changed_validation_regex(self, entry):
		self._do_regex_validation(self.gobjects['entry_test_validation_text'].get_text(), entry)

	def signal_radiobutton_toggled(self, radiobutton):
		if not radiobutton.get_active():
			return
		if radiobutton == self.gobjects['radiobutton_company_existing']:
			self.gobjects['frame_company_existing'].set_sensitive(True)
			self.gobjects['frame_company_new'].set_sensitive(False)
		elif radiobutton == self.gobjects['radiobutton_company_new']:
			self.gobjects['frame_company_existing'].set_sensitive(False)
			self.gobjects['frame_company_new'].set_sensitive(True)
		elif radiobutton == self.gobjects['radiobutton_company_none']:
			self.gobjects['frame_company_existing'].set_sensitive(False)
			self.gobjects['frame_company_new'].set_sensitive(False)

	def interact(self):
		self.assistant.show_all()
Beispiel #12
0
class CampaignViewGenericTableTab(CampaignViewGenericTab):
    """
	This object is meant to be subclassed by tabs which will display
	campaign information of different types from specific database
	tables. The data in this object is refreshed when multiple events
	occur and it uses an internal timer to represent the last time the
	data was refreshed.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('button_refresh', 'treeview_campaign'))
    node_query = None
    """
	The GraphQL query used to load a particular node from the remote table.
	This query is provided with a single parameter of the node's id.
	"""
    table_name = ''
    """The database table represented by this tab."""
    table_query = None
    """
	The GraphQL query used to load the desired information from the remote
	table. This query is provided with the following three parameters:
	campaign, count and cursor.
	"""
    view_columns = ()
    """The dictionary map of column numbers to column names starting at column 1."""
    xlsx_worksheet_options = None

    def __init__(self, *args, **kwargs):
        super(CampaignViewGenericTableTab, self).__init__(*args, **kwargs)
        treeview = self.gobjects['treeview_campaign']
        self.treeview_manager = managers.TreeViewManager(
            treeview,
            selection_mode=Gtk.SelectionMode.MULTIPLE,
            cb_delete=self._prompt_to_delete_row,
            cb_refresh=self.load_campaign_information)
        self.treeview_manager.set_column_titles(self.view_columns,
                                                column_offset=1)
        self.popup_menu = self.treeview_manager.get_popup_menu()
        """The :py:class:`Gtk.Menu` object which is displayed when right-clicking in the view area."""
        treeview = self.gobjects['treeview_campaign']
        store_columns = [str] * (len(self.view_columns) + 1)
        store = Gtk.ListStore(*store_columns)
        treeview.set_model(store)
        self.application.connect('server-connected',
                                 self.signal_kp_server_connected)

    def signal_kp_server_connected(self, _):
        event_id = 'db-' + self.table_name.replace('_', '-')
        server_events = self.application.server_events
        if not server_events:
            return
        server_events.subscribe(event_id, ('deleted', 'inserted', 'updated'),
                                ('id', 'campaign_id'))
        server_events.connect(event_id, self.signal_server_event_db)

    def signal_server_event_db(self, _, event_type, rows):
        get_node = lambda id: self.rpc.graphql(self.node_query,
                                               {'id': str(id)})['db']['node']
        for row in rows:
            if str(row.campaign_id) != self.config['campaign_id']:
                continue
            model = self.gobjects['treeview_campaign'].get_model()
            for case in utilities.switch(event_type):
                if case('inserted'):
                    row_data = self.format_node_data(get_node(row.id))
                    row_data = list(map(self.format_cell_data, row_data))
                    row_data.insert(0, str(row.id))
                    gui_utilities.glib_idle_add_wait(model.append, row_data)
                ti = gui_utilities.gtk_list_store_search(model, str(row.id))
                if ti is None:
                    self.logger.warning(
                        "received server db event: {0} for non-existent row {1}:{2}"
                        .format(event_type, self.table_name, str(row.id)))
                    break
                if case('deleted'):
                    model.remove(ti)
                    break
                if case('updated'):
                    row_data = self.format_node_data(get_node(row.id))
                    for idx, cell_data in enumerate(row_data, 1):
                        model[ti][idx] = self.format_cell_data(cell_data)
                    break

    def _prompt_to_delete_row(self, treeview, _):
        if isinstance(self.loader_thread,
                      threading.Thread) and self.loader_thread.is_alive():
            gui_utilities.show_dialog_warning(
                'Can Not Delete Rows While Loading', self.parent)
            return
        model = treeview.get_model()
        row_ids = [
            model.get_value(ti, 0)
            for ti in gui_utilities.gtk_treeview_selection_iterate(treeview)
        ]
        if len(row_ids) == 0:
            return
        elif len(row_ids) == 1:
            message = 'Delete This Row?'
        else:
            message = "Delete These {0:,} Rows?".format(len(row_ids))
        if not gui_utilities.show_dialog_yes_no(
                message, self.parent, 'This information will be lost.'):
            return
        self.application.emit(self.table_name[:-1] + '-delete', row_ids)

    def format_node_data(self, node):
        """
		This method is overridden by subclasses to format the raw node
		data returned from the server. The length of the list must equal
		the number of columns in the table. This method is called for
		each node in the remote table by the loader thread.

		:param dict node: The node from a GraphQL query representing data for this table.
		:return: The formatted row data.
		:rtype: list
		"""
        raise NotImplementedError()

    def format_cell_data(self, cell_data, encoding='utf-8'):
        """
		This method provides formatting to the individual cell values returned
		from the :py:meth:`.format_row_data` function. Values are converted into
		a format suitable for reading.

		:param cell: The value to format.
		:param str encoding: The encoding to use to coerce the return value into a unicode string.
		:return: The formatted cell value.
		:rtype: str
		"""
        if isinstance(cell_data, datetime.datetime):
            cell_data = utilities.datetime_utc_to_local(cell_data)
            return utilities.format_datetime(cell_data, encoding=encoding)

        if cell_data is None:
            cell_data = ''
        elif isinstance(cell_data, int):
            cell_data = str(cell_data)

        # ensure that the return value is a unicode string
        if isinstance(cell_data, bytes):
            cell_data = cell_data.decode(encoding)
        return cell_data

    def load_campaign_information(self, force=True):
        """
		Load the necessary campaign information from the remote server.
		Unless *force* is True, the
		:py:attr:`~.CampaignViewGenericTab.last_load_time` is compared
		with the :py:attr:`~.CampaignViewGenericTab.refresh_frequency` to
		check if the information is stale. If the local data is not stale,
		this function will return without updating the table.

		:param bool force: Ignore the load life time and force loading the remote data.
		"""
        if not force and (
            (time.time() - self.last_load_time) < self.refresh_frequency):
            return
        self.loader_thread_lock.acquire()
        self._sync_loader_thread()
        self.loader_thread_stop.clear()
        store = self.gobjects['treeview_campaign'].get_model()
        store.clear()
        self.loader_thread = threading.Thread(
            target=self.loader_thread_routine, args=(store, ))
        self.loader_thread.daemon = True
        self.loader_thread.start()
        self.loader_thread_lock.release()
        return

    def loader_thread_routine(self, store):
        """
		The loading routine to be executed within a thread.

		:param store: The store object to place the new data.
		:type store: :py:class:`Gtk.ListStore`
		"""
        gui_utilities.glib_idle_add_wait(lambda: self.gobjects[
            'treeview_campaign'].set_property('sensitive', False))
        campaign_id = self.config['campaign_id']
        count = 500
        page_info = {'endCursor': None, 'hasNextPage': True}
        while page_info['hasNextPage']:
            if self.rpc is None:
                break
            results = self.rpc.graphql(
                self.table_query, {
                    'campaign': campaign_id,
                    'count': count,
                    'cursor': page_info['endCursor']
                })
            if self.loader_thread_stop.is_set():
                break
            if self.is_destroyed.is_set():
                break
            for edge in results['db']['campaign'][self.table_name]['edges']:
                node = edge['node']
                row_data = self.format_node_data(node)
                row_data = list(map(self.format_cell_data, row_data))
                row_data.insert(0, str(node['id']))
                gui_utilities.glib_idle_add_wait(store.append, row_data)
            page_info = results['db']['campaign'][self.table_name]['pageInfo']
        if self.is_destroyed.is_set():
            return
        gui_utilities.glib_idle_add_wait(lambda: self.gobjects[
            'treeview_campaign'].set_property('sensitive', True))
        self.last_load_time = time.time()

    def signal_button_clicked_export(self, button):
        self.export_table_to_csv()

    def export_table_to_csv(self):
        """Export the data represented by the view to a CSV file."""
        if not self.loader_thread_lock.acquire(False) or (
                isinstance(self.loader_thread, threading.Thread)
                and self.loader_thread.is_alive()):
            gui_utilities.show_dialog_warning(
                'Can Not Export Rows While Loading', self.parent)
            return
        dialog = extras.FileChooserDialog('Export Data', self.parent)
        file_name = self.config['campaign_name'] + '.csv'
        response = dialog.run_quick_save(file_name)
        dialog.destroy()
        if not response:
            self.loader_thread_lock.release()
            return
        destination_file = response['target_path']
        store = self.gobjects['treeview_campaign'].get_model()
        columns = dict(enumerate(('UID', ) + self.view_columns))
        export.liststore_to_csv(store, destination_file, columns)
        self.loader_thread_lock.release()

    def export_table_to_xlsx_worksheet(self, worksheet, title_format):
        """
		Export the data represented by the view to an XLSX worksheet.

		:param worksheet: The destination sheet for the store's data.
		:type worksheet: :py:class:`xlsxwriter.worksheet.Worksheet`
		:param title_format: The formatting to use for the title row.
		:type title_format: :py:class:`xlsxwriter.format.Format`
		"""
        if not self.loader_thread_lock.acquire(False) or (
                isinstance(self.loader_thread, threading.Thread)
                and self.loader_thread.is_alive()):
            gui_utilities.show_dialog_warning(
                'Can Not Export Rows While Loading', self.parent)
            return
        store = self.gobjects['treeview_campaign'].get_model()
        columns = dict(enumerate(('UID', ) + self.view_columns))
        export.liststore_to_xlsx_worksheet(
            store,
            worksheet,
            columns,
            title_format,
            xlsx_options=self.xlsx_worksheet_options)
        self.loader_thread_lock.release()
Beispiel #13
0
class CampaignViewDashboardTab(CampaignViewGenericTab):
    """Display campaign information on a graphical dash board."""
    dependencies = gui_utilities.GladeDependencies(
        children=('box_top_left', 'box_top_right', 'box_bottom',
                  'scrolledwindow_top_left', 'scrolledwindow_top_right',
                  'scrolledwindow_bottom'))
    label_text = 'Dashboard'
    """The tabs label for display in the GUI."""
    def __init__(self, *args, **kwargs):
        super(CampaignViewDashboardTab, self).__init__(*args, **kwargs)
        self.graphs = []
        """The :py:class:`.CampaignGraph` classes represented on the dash board."""

        dash_ports = {
            # dashboard position, (width, height)
            'top_left': (380, 200),
            'top_right': (380, 200),
            'bottom': (760, 200)
        }
        for dash_port, details in dash_ports.items():
            graph_name = self.config['dashboard.' + dash_port]
            cls = graphs.get_graph(graph_name)
            if not cls:
                self.logger.warning('could not get graph: ' + graph_name)
                logo_file_path = find.data_file('king-phisher-icon.svg')
                if logo_file_path:
                    image = Gtk.Image.new_from_pixbuf(
                        GdkPixbuf.Pixbuf.new_from_file_at_size(
                            logo_file_path, 128, 128))
                    image.show()
                    self.gobjects['scrolledwindow_' + dash_port].add(image)
                continue
            graph_inst = cls(
                self.application, details,
                getattr(self, self.top_gobject).get_style_context())
            self.gobjects['scrolledwindow_' + dash_port].add(graph_inst.canvas)
            self.gobjects['box_' + dash_port].pack_end(
                graph_inst.navigation_toolbar, False, False, 0)
            self.graphs.append(graph_inst)
        self.logger.debug(
            "dashboard refresh frequency set to {0} seconds".format(
                self.refresh_frequency))
        GLib.timeout_add_seconds(self.refresh_frequency,
                                 self.loader_idle_routine)

    def load_campaign_information(self, force=True):
        """
		Load the necessary campaign information from the remote server.
		Unless *force* is True, the :py:attr:`~.last_load_time` is compared with
		the :py:attr:`~.refresh_frequency` to check if the information is stale.
		If the local data is not stale, this function will return without
		updating the table.

		:param bool force: Ignore the load life time and force loading the remote data.
		"""
        if not force and (
            (time.time() - self.last_load_time) < self.refresh_frequency):
            return
        if not self.application.rpc:
            self.logger.warning(
                'skipping load_campaign_information because rpc is not initialized'
            )
            return
        with self.loader_thread_lock:
            self._sync_loader_thread()
            self.loader_thread_stop.clear()
            self.loader_thread = threading.Thread(
                target=self.loader_thread_routine)
            self.loader_thread.daemon = True
            self.loader_thread.start()

    def loader_idle_routine(self):
        """The routine which refreshes the campaign data at a regular interval."""
        if self.rpc and not self.loader_thread_is_running:
            self.logger.debug('idle loader routine called')
            self.load_campaign_information()
        return True

    def loader_thread_routine(self):
        """The loading routine to be executed within a thread."""
        if not 'campaign_id' in self.config:
            return
        if not self._get_graphql_campaign():
            return
        info_cache = {}
        for graph in self.graphs:
            if self.loader_thread_stop.is_set():
                break
            if self.is_destroyed.is_set():
                break
            info_cache.update(
                gui_utilities.glib_idle_add_wait(lambda g=graph: g.refresh(
                    info_cache, self.loader_thread_stop)))
        else:
            self.last_load_time = time.time()

    def _get_graphql_campaign(self, campaign_id=None):
        results = self.rpc.graphql(
            """\
		query getCampaign($id: String!) {
			db {
				campaign(id: $id) {
					name
				}
			}
		}""", {'id': campaign_id or self.config['campaign_id']})
        return results['db'].get('campaign', None)
class ImportCampaignWindow(gui_utilities.GladeGObject):
    """
	Display a dialog which allows a new campaign to be created or an
	existing campaign to be opened.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('progressbar', 'textview', 'entry_campaign', 'entry_file',
                  'button_import', 'button_select', 'spinner'),
        top_level=('ImportCampaignWindow', ))
    top_gobject = 'window'

    def __init__(self, *args, **kwargs):
        super(ImportCampaignWindow, self).__init__(*args, **kwargs)
        self.logger = logging.getLogger(
            'KingPhisher.Client.ImportCampaignWindow')
        self.rpc = self.application.rpc
        self.button_import_campaign = self.gobjects['button_import']
        self.button_import_campaign.set_sensitive(False)
        self.button_select = self.gobjects['button_select']
        self.entry_campaign_name = self.gobjects['entry_campaign']
        self.entry_path = self.gobjects['entry_file']
        self.import_progress = self.gobjects['progressbar']
        self.spinner = self.gobjects['spinner']
        self.text_buffer = self.gobjects['textview'].get_buffer()

        # place holders for once an xml file is loaded
        self.campaign_info = None
        self.db_campaigns = None
        self.thread_import_campaign = None
        self.window.show_all()

    def _set_text_view(self, string_to_set):
        GLib.idle_add(self.text_buffer.set_text, string_to_set + '\n')

    def __update_text_view(self, string_to_add):
        end_iter = self.text_buffer.get_end_iter()
        self.text_buffer.insert(end_iter, string_to_add + '\n')

    def _update_text_view(self, string_to_add, idle=False):
        if idle:
            GLib.idle_add(self.__update_text_view, string_to_add)
        else:
            self.__update_text_view(string_to_add)

    def _update_id(self, element, id_fields, old_id, new_id):
        """
		Iterates through the element and replaces the specified old ID with the
		new ID in the requested ID fields.

		:param element: Element to iterate over where the old id values can be found.
		:type element: :py:class:`xml.etree.ElementTree.Element`
		:param list id_fields: The list of fields to look for old_id.
		:param old_id: The old id value that has been changed
		:param new_id: The new id value to set.
		"""
        for nods in element.iter():
            if nods.tag in id_fields and nods.text == old_id:
                nods.text = new_id
                # if new_id is none set type to null
                if new_id is None:
                    nods.attrib['type'] = 'null'

    def _get_keys_values(self, element):
        keys = []
        rows = []
        for subelements in element:
            values = []
            for node in subelements:
                keys.append(node.tag) if node.tag not in keys else None
                value = serializers.from_elementtree_element(node)
                if isinstance(
                        value,
                    (datetime.datetime, datetime.date, datetime.time)):
                    value = value.replace(
                        tzinfo=None)  # remove the UTC timezone info
                values.append(value)
            rows.append(tuple(values))
        return keys, rows

    def signal_entry_change(self, _):
        """
		When there is a change in the campaign entry field it will check to see
		if the name is already in use. If it is not in use it will change the
		sensitivity of the :py:attr:`.button_import_campaign` to allow the user
		to start the import process.
		"""
        if not self.campaign_info:
            return
        if not self._check_campaign_name(self.entry_campaign_name.get_text()):
            self.button_import_campaign.set_sensitive(False)
            return
        self.button_import_campaign.set_sensitive(True)
        return

    def signal_multi_open_xml_file(self, _):
        if not self.button_select.get_property('sensitive'):
            return
        self.select_xml_campaign()

    def signal_window_delete_event(self, _, event):
        """
		Checks to make sure the import campaign thread is closed before
		closing the window.
		"""
        if not self.campaign_info:
            return False
        if not self.thread_import_campaign:
            return False
        if not self.thread_import_campaign.is_alive():
            return False
        response = gui_utilities.show_dialog_yes_no(
            'Cancel Importing?', self.window,
            'Do you want to cancel importing the campaign?')
        if not response:
            return True

        self.thread_import_campaign.stop()
        self.thread_import_campaign.join()
        self._import_cleanup(remove_campaign=True)

    def failed_import_action(self):
        response = gui_utilities.show_dialog_yes_no(
            'Failed to import campaign', self.window,
            'Import failed, do you want to cancel and delete this campaign?')
        return response

    def remove_import_campaign(self):
        """
		Used to delete the imported campaign on failure or early exit of the
		import window, if the user selects to have it removed.
		"""
        campaign_id = self.campaign_info.find('id').text
        campaign_name = self.campaign_info.find('name').text
        campaign_check = self.rpc('db/table/get', 'campaigns', campaign_id)
        if not campaign_check:
            return
        if campaign_name == campaign_check['name']:
            self.rpc('db/table/delete', 'campaigns', campaign_id)
            self.logger.info("deleted campaign {}".format(campaign_id))

    def signal_import_button(self, _):
        """
		This will check to see if the campaign information is present. If
		campaign information is present it will launch an py:class:`ImportThread`
		to import the campaign in the background, freeing up the GUI for the
		user to conduct other functions.
		"""
        if not self.campaign_info:
            self._update_text_view('No campaign information to import')
            self.button_import_campaign.set_sensitive(False)
            return
        self.thread_import_campaign = ImportThread(
            target=self._import_campaign)
        self.thread_import_campaign.start()

    def select_xml_campaign(self):
        """
		Prompts the user with a file dialog window to select the King Phisher
		Campaign XML file to import. Validates the file to make sure it is a
		Campaign exported from King Phisher and is the correct version to import.
		"""
        dialog = extras.FileChooserDialog('Import Campaign from XML',
                                          self.window)
        dialog.quick_add_filter('King Phisher XML Campaign', '*.xml')
        dialog.quick_add_filter('All Files', '*')
        response = dialog.run_quick_open()
        dialog.destroy()
        if not response:
            return
        target_file = response['target_path']
        self.entry_path.set_text(target_file)

        try:
            campaign_xml = ET.parse(target_file)
        except ET.ParseError as error:
            self.logger.error(
                "cannot import campaign file: {0} (not a valid xml file)".
                format(target_file))
            gui_utilities.show_dialog_error('Improper Format', self.window,
                                            'File is not valid XML')
            return

        root = campaign_xml.getroot()
        if root.tag != 'king_phisher':
            self.logger.error(
                "cannot import campaign file: {0} (invalid root xml tag)".
                format(target_file))
            gui_utilities.show_dialog_error(
                'Improper Format', self.window,
                'File is not a valid King Phisher XML campaign file')
            return

        meta_data = root.find('metadata')
        if meta_data.find('version').text < '1.3':
            self.logger.error(
                "cannot import campaign file: {0} (incompatible version)".
                format(target_file))
            gui_utilities.show_dialog_error(
                'Invalid Version', self.window,
                'Cannot import XML campaign data less then version 1.3')
            return

        self.campaign_info = root.find('campaign')
        if not self.campaign_info:
            self.logger.error(
                "cannot import campaign file: {0} (no campaign data found)".
                format(target_file))
            gui_utilities.show_dialog_error('No Campaign Data', self.window,
                                            'No campaign data to import')
            return

        self.db_campaigns = self.rpc.graphql(
            "{ db { campaigns { edges { node { id, name } } } } }"
        )['db']['campaigns']['edges']
        self.entry_campaign_name.set_text(self.campaign_info.find('name').text)
        self.thread_import_campaign = None
        if not self._check_campaign_name(self.campaign_info.find('name').text,
                                         verbose=True):
            self.button_import_campaign.set_sensitive(False)
            return
        self.button_import_campaign.set_sensitive(True)

    def _check_campaign_name(self, campaign_name, verbose=False):
        """
		Will check to see if the provided campaign name is safe to use.

		:param str campaign_name: campaign name to check
		:param bool verbose: If true will update output to text buffer.
		:return: True if campaign name can be used
		:rtype: bool
		"""
        if not self.campaign_info or not self.db_campaigns:
            return False

        if next((nodes for nodes in self.db_campaigns
                 if nodes['node']['name'] == campaign_name), None):
            if verbose:
                self._update_text_view(
                    "Campaign name {} is already in use by another campaign.".
                    format(campaign_name),
                    idle=True)
            return False

        if verbose:
            self._update_text_view(
                "Campaign Name {} is not in use, ready to import".format(
                    campaign_name),
                idle=True)
        return True

    def preprep_xml_data(self):
        """
		This function provides the actions required to see if required IDs are
		already in the database. If they are not it will clear them out and set
		subelement.attrib['type'] to null. If the element is required it will
		set it to a default value. This will normalize the data and ready it for
		import into the database.
		"""
        self._set_text_view('Normalizing Campaign Data')
        self.campaign_info.find(
            'name').text = self.entry_campaign_name.get_text()

        campaign_type_check = self.rpc(
            'db/table/get', 'campaign_types',
            self.campaign_info.find('campaign_type_id').text)
        if not campaign_type_check:
            temp_string = 'Campaign type not found, removing'
            self.logger.info(temp_string.lower())
            self._update_text_view(temp_string, idle=True)
            reset_node = self.campaign_info.find('campaign_type_id')
            reset_node.clear()
            reset_node.attrib['type'] = 'null'

        if self.campaign_info.find(
                'user_id').text != self.config['server_username']:
            temp_string = 'Setting the campaign owner to the current user'
            self.logger.info(temp_string.lower())
            self._update_text_view(temp_string, idle=True)
            self.campaign_info.find(
                'user_id').text = self.config['server_username']

        company_id_check = self.rpc(
            'db/table/get', 'companies',
            int(self.campaign_info.find('company_id').text))
        if not company_id_check:
            temp_string = 'Company id not found, removing'
            self.logger.info(temp_string.lower())
            self._update_text_view(temp_string, idle=True)
            reset_node = self.campaign_info.find('company_id')
            reset_node.clear()
            reset_node.attrib['type'] = 'null'

        for message in self.campaign_info.find('messages').getiterator():
            if message.tag != 'company_department_id':
                continue
            if not message.text:
                continue
            self.logger.info("checking company_department_id {}".format(
                message.text))
            company_department_id_check = self.rpc('db/table/get',
                                                   'company_departments',
                                                   message.text)
            if not company_department_id_check:
                temp_string = "Company department id {} not found, removing it from campaign".format(
                    message.text)
                self.logger.info(temp_string.lower())
                self._update_text_view(temp_string, idle=True)
                self._update_id(self.campaign_info, ['company_department_id'],
                                message.text, None)

    def _import_cleanup(self, remove_campaign=False):
        if remove_campaign:
            self.remove_import_campaign()
        GLib.idle_add(self.button_import_campaign.set_sensitive, False)
        GLib.idle_add(self.button_select.set_sensitive, True)
        GLib.idle_add(self.spinner.stop)
        self.campaign_info = None

    def _import_campaign(self):
        """
		Used by the import thread to import the campaign into the database.
		Through this process after every major action, the thread will check
		to see if it has been requested to stop.
		"""
        self.logger.debug("import campaign running in tid: 0x{0:x}".format(
            threading.current_thread().ident))
        if not self.campaign_info:
            return
        # prevent user from changing campaign info during import
        start_time = datetime.datetime.now()
        GLib.idle_add(self.button_import_campaign.set_sensitive, False)
        GLib.idle_add(self.button_select.set_sensitive, False)
        GLib.idle_add(self.spinner.start)

        batch_size = 100
        if self.thread_import_campaign.stopped():
            return
        self.preprep_xml_data()

        self.campaign_info.find('id').text = self.rpc(
            'campaign/new',
            self.campaign_info.find('name').text,
            self.campaign_info.find('description').text)
        self.logger.info("created new campaign id: {}".format(
            self.campaign_info.find('id').text))

        nodes_completed = 0
        node_count = float(len(self.campaign_info.findall('.//*')))
        if self.thread_import_campaign.stopped():
            return
        for nods in self.campaign_info.getiterator():
            if nods.tag == 'campaign_id':
                nods.text = self.campaign_info.find('id').text
        self._update_text_view("Campaign created, ID set to {}".format(
            self.campaign_info.find('id').text),
                               idle=True)

        keys = []
        values = []
        if self.thread_import_campaign.stopped():
            return
        for elements in self.campaign_info:
            if elements.tag in ('id', 'landing_pages', 'messages', 'visits',
                                'credentials', 'deaddrop_deployments',
                                'deaddrop_connections'):
                continue
            keys.append(elements.tag)
            values.append(elements.text)

        self.rpc('db/table/set', 'campaigns',
                 int(self.campaign_info.find('id').text), tuple(keys),
                 tuple(values))
        nodes_completed += float(len(values) + 1)
        percentage_completed = nodes_completed / node_count
        GLib.idle_add(self.import_progress.set_fraction, percentage_completed)
        if self.thread_import_campaign.stopped():
            return

        for tables in ('landing_pages', 'messages', 'visits', 'credentials',
                       'deaddrop_deployments', 'deaddrop_connections'):
            inserted_ids = []
            if self.thread_import_campaign.stopped():
                return
            self._update_text_view(
                "Serializing table {} data for import".format(tables),
                idle=True)
            keys, rows = self._get_keys_values(self.campaign_info.find(tables))
            self._update_text_view("Working on table {} adding {} rows".format(
                tables, len(rows)),
                                   idle=True)
            if self.thread_import_campaign.stopped():
                return

            # make table rows easy to manage for updating new ids returned
            table_rows = []
            for row in rows:
                row = dict(zip(keys, row))
                table_rows.append(row)

            while rows and not self.thread_import_campaign.stopped():
                try:
                    inserted_ids = inserted_ids + self.rpc(
                        '/db/table/insert/multi',
                        tables,
                        keys,
                        rows[:batch_size],
                        deconflict_ids=True)
                except advancedhttpserver.RPCError:
                    response = gui_utilities.glib_idle_add_wait(
                        self.failed_import_action)
                    self._import_cleanup(remove_campaign=response)
                    failed_string = 'Failed to import campaign, all partial campaign data ' + (
                        'has been removed'
                        if response else 'was left in place')
                    self.logger.warning(failed_string.lower())
                    self._update_text_view(failed_string, idle=True)
                    return

                rows = rows[batch_size:]
                nodes_completed += float(batch_size * len(keys))
                percentage_completed = nodes_completed / node_count
                GLib.idle_add(self.import_progress.set_fraction,
                              percentage_completed)

            if self.thread_import_campaign.stopped():
                return

            # update id fields to maintain relationships
            self._update_text_view(
                "Updating dependencies for table: {}".format(tables),
                idle=True)
            for id_ in inserted_ids:
                if id_ != table_rows[inserted_ids.index(id_)]['id']:
                    self._update_id(self.campaign_info,
                                    ['id', "{}_id".format(tables[:-1])],
                                    table_rows[inserted_ids.index(id_)]['id'],
                                    id_)

        GLib.idle_add(self.import_progress.set_fraction, 1.0)
        self._import_cleanup()
        done_string = "Done importing campaign. Importing the campaign took {}".format(
            datetime.datetime.now() - start_time)
        self._update_text_view(done_string, idle=True)
        self.logger.info(done_string.lower())
Beispiel #15
0
class PluginManagerWindow(gui_utilities.GladeGObject):
    """
	The window which allows the user to selectively enable and disable plugins
	for the client application. This also handles configuration changes, so the
	enabled plugins will persist across application runs.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('expander_plugin_info', 'grid_plugin_info',
                  'label_plugin_info_authors',
                  'label_plugin_info_for_compatible',
                  'label_plugin_info_compatible',
                  'label_plugin_info_description',
                  'label_plugin_info_homepage', 'label_plugin_info_title',
                  'label_plugin_info_version', 'paned_plugins',
                  'scrolledwindow_plugins', 'stack_plugin_info',
                  'treeview_plugins', 'textview_plugin_info',
                  'viewport_plugin_info'))
    top_gobject = 'window'

    def __init__(self, *args, **kwargs):
        super(PluginManagerWindow, self).__init__(*args, **kwargs)
        treeview = self.gobjects['treeview_plugins']
        self._last_plugin_selected = None
        self._module_errors = {}
        tvm = managers.TreeViewManager(treeview, cb_refresh=self.load_plugins)
        toggle_renderer = Gtk.CellRendererToggle()
        toggle_renderer.connect('toggled', self.signal_renderer_toggled)
        tvm.set_column_titles(
            ('Enabled', 'Plugin'),
            column_offset=1,
            renderers=(toggle_renderer, Gtk.CellRendererText()))
        tvm.column_views['Enabled'].set_cell_data_func(
            toggle_renderer, self._toggle_cell_data_func)
        self._model = Gtk.ListStore(str, bool, str)
        self._model.set_sort_column_id(2, Gtk.SortType.ASCENDING)
        treeview.set_model(self._model)
        self.load_plugins()

        self.popup_menu = tvm.get_popup_menu()
        self.popup_menu.append(Gtk.SeparatorMenuItem())
        menu_item = Gtk.MenuItem.new_with_label('Reload')
        menu_item.connect('activate', self.signal_popup_menu_activate_reload)
        self.popup_menu.append(menu_item)
        self.popup_menu.show_all()

        self.window.show()
        selection = treeview.get_selection()
        selection.unselect_all()
        paned = self.gobjects['paned_plugins']
        self._paned_offset = paned.get_allocation(
        ).height - paned.get_position()

    def _on_plugin_load_error(self, name, error):
        self._module_errors[name] = (error,
                                     traceback.format_exception(
                                         *sys.exc_info(), limit=5))

    def _toggle_cell_data_func(self, column, cell, model, tree_iter, _):
        if model.get_value(tree_iter, 0) in self._module_errors:
            cell.set_property('inconsistent', True)
        else:
            cell.set_property('inconsistent', False)

    def load_plugins(self):
        """
		Load the plugins which are available into the treeview to make them
		visible to the user.
		"""
        store = self._model
        store.clear()
        pm = self.application.plugin_manager
        self._module_errors = {}
        pm.load_all(on_error=self._on_plugin_load_error)
        for name, plugin in pm.loaded_plugins.items():
            store.append((plugin.name, plugin.name
                          in pm.enabled_plugins, plugin.title))
        for name in self._module_errors.keys():
            store.append((name, False, "{0} (Load Failed)".format(name)))

    def signal_label_activate_link(self, _, uri):
        utilities.open_uri(uri)

    def signal_eventbox_button_press(self, widget, event):
        if not (event.type == Gdk.EventType.BUTTON_PRESS
                and event.button == Gdk.BUTTON_PRIMARY):
            return
        name = self._last_plugin_selected
        if name is None:
            return
        klass = self.application.plugin_manager[name]
        popover = Gtk.Popover()
        popover.set_relative_to(
            self.gobjects['label_plugin_info_for_compatible'])
        grid = Gtk.Grid()
        popover.add(grid)
        grid.insert_column(0)
        grid.insert_column(0)
        grid.insert_column(0)
        grid.set_column_spacing(3)
        row = 0
        for req in klass.compatibility:
            grid.insert_row(row)
            label = Gtk.Label(req[0])
            label.set_property('halign', Gtk.Align.START)
            grid.attach(label, 0, row, 1, 1)
            label = Gtk.Label(req[1])
            label.set_property('halign', Gtk.Align.START)
            grid.attach(label, 1, row, 1, 1)
            label = Gtk.Label('Yes' if req[2] else 'No')
            label.set_property('halign', Gtk.Align.END)
            grid.attach(label, 2, row, 1, 1)
            row += 1
        if not row:
            popover.destroy()
            return
        popover.show_all()

    def signal_expander_activate(self, expander):
        paned = self.gobjects['paned_plugins']
        if expander.get_property('expanded'):  # collapsing
            paned.set_position(paned.get_allocation().height +
                               self._paned_offset)

    def signal_paned_button_press_event(self, paned, event):
        return not self.gobjects['expander_plugin_info'].get_property(
            'expanded')

    def signal_popup_menu_activate_reload(self, _):
        treeview = self.gobjects['treeview_plugins']
        pm = self.application.plugin_manager
        selected_plugin = None
        selection = treeview.get_selection()
        if selection.count_selected_rows():
            (model, tree_paths) = selection.get_selected_rows()
            selected_plugin = model[tree_paths[0]][0]

        for tree_iter in gui_utilities.gtk_treeview_selection_iterate(
                treeview):
            name = self._model[tree_iter][0]  # pylint: disable=unsubscriptable-object
            enabled = name in pm.enabled_plugins
            pm.unload(name)
            try:
                klass = pm.load(name, reload_module=True)
            except Exception as error:
                self._on_plugin_load_error(name, error)
                if name == selected_plugin:
                    self._set_plugin_info(name)
                self._model[tree_iter][2] = "{0} (Reload Failed)".format(name)  # pylint: disable=unsubscriptable-object
                continue
            if name in self._module_errors:
                del self._module_errors[name]
                self._model[tree_iter][2] = klass.title  # pylint: disable=unsubscriptable-object
            if name == selected_plugin:
                self._set_plugin_info(name)
            if enabled:
                pm.enable(name)

    def signal_renderer_toggled(self, _, path):
        pm = self.application.plugin_manager
        name = self._model[path][0]  # pylint: disable=unsubscriptable-object
        if name in self._module_errors:
            gui_utilities.show_dialog_error(
                'Can Not Enable Plugin', self.window,
                'Can not enable a plugin which failed to load.')
            return
        if self._model[path][1]:  # pylint: disable=unsubscriptable-object
            pm.disable(name)
            self._model[path][1] = False  # pylint: disable=unsubscriptable-object
            self.config['plugins.enabled'].remove(name)
        else:
            if not pm.loaded_plugins[name].is_compatible:
                gui_utilities.show_dialog_error(
                    'Incompatible Plugin', self.window,
                    'This plugin is not compatible.')
                return
            if not pm.enable(name):
                return
            self._model[path][1] = True  # pylint: disable=unsubscriptable-object
            self.config['plugins.enabled'].append(name)

    def signal_treeview_row_activated(self, treeview, path, column):
        name = self._model[path][0]  # pylint: disable=unsubscriptable-object
        self._set_plugin_info(name)

    def _set_plugin_info(self, name):
        stack = self.gobjects['stack_plugin_info']
        textview = self.gobjects['textview_plugin_info']
        buf = textview.get_buffer()
        buf.delete(buf.get_start_iter(), buf.get_end_iter())
        if name in self._module_errors:
            stack.set_visible_child(textview)
            self._set_plugin_info_error(name)
        else:
            stack.set_visible_child(self.gobjects['grid_plugin_info'])
            self._set_plugin_info_details(name)

    def _set_plugin_info_details(self, name):
        pm = self.application.plugin_manager
        self._last_plugin_selected = name
        klass = pm.loaded_plugins[name]
        self.gobjects['label_plugin_info_title'].set_text(klass.title)
        self.gobjects['label_plugin_info_compatible'].set_text(
            'Yes' if klass.is_compatible else 'No')
        self.gobjects['label_plugin_info_version'].set_text(klass.version)
        self.gobjects['label_plugin_info_authors'].set_text('\n'.join(
            klass.authors))
        label_homepage = self.gobjects['label_plugin_info_homepage']
        if klass.homepage is None:
            label_homepage.set_property('visible', False)
        else:
            label_homepage.set_markup("<a href=\"{0}\">Homepage</a>".format(
                klass.homepage))
            label_homepage.set_property('tooltip-text', klass.homepage)
            label_homepage.set_property('visible', True)
        self.gobjects['label_plugin_info_description'].set_text(
            klass.formatted_description)

    def _set_plugin_info_error(self, name):
        textview = self.gobjects['textview_plugin_info']
        exc, formatted_exc = self._module_errors[name]
        buf = textview.get_buffer()
        buf.insert(buf.get_end_iter(), "{0!r}\n\n".format(exc), -1)
        buf.insert(buf.get_end_iter(), ''.join(formatted_exc), -1)
class PluginManagerWindow(gui_utilities.GladeGObject):
    """
	The window which allows the user to selectively enable and disable plugins
	for the client application. This also handles configuration changes, so the
	enabled plugins will persist across application runs.
	"""
    dependencies = gui_utilities.GladeDependencies(children=(
        'expander_info', 'grid_catalog_repo_info', 'grid_plugin_info',
        'label_catalog_repo_info_title', 'label_catalog_repo_info_description',
        'label_catalog_repo_info_for_description',
        'label_catalog_repo_info_homepage',
        'label_catalog_repo_info_maintainers',
        'label_catalog_repo_info_for_maintainers', 'label_plugin_info_authors',
        'label_plugin_info_for_compatible', 'label_plugin_info_compatible',
        'label_plugin_info_description', 'label_plugin_info_homepage',
        'label_plugin_info_title', 'label_plugin_info_version',
        'paned_plugins', 'scrolledwindow_plugins', 'stack_info',
        'treeview_plugins', 'textview_plugin_info', 'viewport_info',
        'statusbar'))
    top_gobject = 'window'
    _named_model = collections.namedtuple('model_row', [
        'id', 'installed', 'enabled', 'title', 'compatibility', 'version',
        'visible_enabled', 'visible_installed', 'sensitive_installed', 'type'
    ])

    def __init__(self, *args, **kwargs):
        super(PluginManagerWindow, self).__init__(*args, **kwargs)
        treeview = self.gobjects['treeview_plugins']
        self.status_bar = self.gobjects['statusbar']
        self._module_errors = {}
        tvm = managers.TreeViewManager(treeview, cb_refresh=self._load_plugins)
        toggle_renderer_enable = Gtk.CellRendererToggle()
        toggle_renderer_enable.connect('toggled',
                                       self.signal_renderer_toggled_enable)
        toggle_renderer_install = Gtk.CellRendererToggle()
        toggle_renderer_install.connect('toggled',
                                        self.signal_renderer_toggled_install)
        tvm.set_column_titles(
            ['Installed', 'Enabled', 'Title', 'Compatible', 'Version'],
            column_offset=1,
            renderers=[
                toggle_renderer_install, toggle_renderer_enable,
                Gtk.CellRendererText(),
                Gtk.CellRendererText(),
                Gtk.CellRendererText()
            ])
        tvm.column_views['Enabled'].set_cell_data_func(
            toggle_renderer_enable, self._toggle_cell_data_func)
        tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable,
                                                  'visible', 6)
        tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable,
                                                  'sensitive', 1)
        tvm.column_views['Installed'].add_attribute(toggle_renderer_install,
                                                    'visible', 7)
        tvm.column_views['Installed'].add_attribute(toggle_renderer_install,
                                                    'sensitive', 8)
        self._model = Gtk.TreeStore(str, bool, bool, str, str, str, bool, bool,
                                    bool, str)
        self._model.set_sort_column_id(3, Gtk.SortType.ASCENDING)
        treeview.set_model(self._model)
        self.plugin_path = os.path.join(self.application.user_data_path,
                                        'plugins')
        self.load_thread = utilities.Thread(target=self._load_catalogs)
        self.load_thread.start()
        self.popup_menu = tvm.get_popup_menu()
        self.popup_menu.append(Gtk.SeparatorMenuItem())
        menu_item = Gtk.MenuItem.new_with_label('Reload')
        menu_item.connect('activate', self.signal_popup_menu_activate_reload)
        self.popup_menu.append(menu_item)
        menu_item_reload_all = Gtk.MenuItem.new_with_label('Reload All')
        menu_item_reload_all.connect(
            'activate', self.signal_popup_menu_activate_reload_all)
        self.popup_menu.append(menu_item_reload_all)
        self.popup_menu.show_all()
        self._update_status_bar('Loading...')
        self.window.show()

        selection = treeview.get_selection()
        selection.unselect_all()
        paned = self.gobjects['paned_plugins']
        self._paned_offset = paned.get_allocation(
        ).height - paned.get_position()

    def _treeview_unselect(self):
        treeview = self.gobjects['treeview_plugins']
        treeview.get_selection().unselect_all()

    def signal_window_show(self, _):
        pass

    def _load_catalogs(self, refresh=False):
        expiration = datetime.timedelta(hours=4)
        self._update_status_bar('Loading, catalogs...', idle=True)
        self.catalog_plugins = plugins.ClientCatalogManager(
            self.application.user_data_path)
        catalog_cache = self.catalog_plugins.get_cache()
        now = datetime.datetime.utcnow()
        for catalog_url in self.config['catalogs']:
            catalog_cache_dict = catalog_cache.get_catalog_by_url(catalog_url)
            if not refresh and catalog_cache_dict and catalog_cache_dict[
                    'created'] + expiration > now:
                try:
                    catalog = Catalog(catalog_cache_dict['value'])
                except (KeyError, TypeError) as error:
                    self.logger.warning(
                        "{0} error when trying to add catalog dict to manager".
                        format(error.__class__.__name))
                else:
                    self.catalog_plugins.add_catalog(
                        catalog,
                        catalog_url=catalog_cache_dict['url'],
                        cache=False)
                    continue
            self.logger.debug("downloading catalog: {}".format(catalog_url))
            self._update_status_bar(
                "Loading, downloading catalog: {}".format(catalog_url))
            catalog = self._load_catalog_from_url(catalog_url)
            if not catalog:
                continue
            self.catalog_plugins.add_catalog(catalog,
                                             catalog_url=catalog_url,
                                             cache=True)
        self._load_plugins()

    def _load_catalog_from_url(self, catalog_url):
        catalog = None
        try:
            catalog = Catalog.from_url(catalog_url)
        except requests.exceptions.ConnectionError:
            self.logger.warning(
                "connection error trying to download catalog url: {}".format(
                    catalog_url))
            self.idle_show_dialog_error(
                'Catalog Loading Error',
                "Failed to download catalog, check your internet connection.")
        except Exception:
            self.logger.warning("failed to add catalog by url", exc_info=True)
            self.idle_show_dialog_error('Catalog Loading Error',
                                        "Failed to add catalog")
        return catalog

    def idle_show_dialog_error(self, title, message):
        gui_utilities.glib_idle_add_once(gui_utilities.show_dialog_error,
                                         title, self.window, message)

    def __update_status_bar(self, string_to_set):
        self.status_bar.pop(0)
        self.status_bar.push(0, string_to_set)

    def _update_status_bar(self, string_to_set, idle=False):
        if idle:
            gui_utilities.glib_idle_add_once(self.__update_status_bar,
                                             string_to_set)
        else:
            self.__update_status_bar(string_to_set)

    def _set_model_item(self, model_path, item, item_value):
        self._model[model_path][self._named_model._fields.index(
            item)] = item_value

    def _on_plugin_load_error(self, name, error):
        self._module_errors[name] = (error,
                                     traceback.format_exception(
                                         *sys.exc_info(), limit=5))

    def _toggle_cell_data_func(self, column, cell, model, tree_iter, _):
        if model.get_value(tree_iter, 0) in self._module_errors:
            cell.set_property('inconsistent', True)
        else:
            cell.set_property('inconsistent', False)

    def _store_append(self, store, parent, model):
        return store.append(parent, model)

    def _store_extend(self, store, parent, models):
        for model in models:
            store.append(parent, model)

    def _load_plugins(self):
        """
		Load the plugins which are available into the treeview to make them
		visible to the user.
		"""
        self.logger.debug('loading plugins')
        self._update_status_bar('Loading plugins...', idle=True)
        store = self._model
        store.clear()
        pm = self.application.plugin_manager
        self._module_errors = {}
        pm.load_all(on_error=self._on_plugin_load_error)
        model = (_LOCAL_REPOSITORY_ID, None, True, _LOCAL_REPOSITORY_TITLE,
                 None, None, False, False, False, _ROW_TYPE_CATALOG)
        catalog_row = gui_utilities.glib_idle_add_wait(self._store_append,
                                                       store, None, model)
        models = []
        for name, plugin in pm.loaded_plugins.items():
            if self.config['plugins.installed'].get(name):
                continue
            self.config['plugins.installed'][name] = None
            models.append(
                self._named_model(
                    id=plugin.name,
                    installed=True,
                    enabled=plugin.name in pm.enabled_plugins,
                    title=plugin.title,
                    compatibility='Yes' if plugin.is_compatible else 'No',
                    version=plugin.version,
                    visible_enabled=True,
                    visible_installed=True,
                    sensitive_installed=False,
                    type=_ROW_TYPE_PLUGIN))
        gui_utilities.glib_idle_add_once(self._store_extend, store,
                                         catalog_row, models)
        del models

        for name in self._module_errors.keys():
            model = (name, True, False, "{0} (Load Failed)".format(name), 'No',
                     'Unknown', True, True, False, _ROW_TYPE_PLUGIN)
            gui_utilities.glib_idle_add_once(self._store_append, store,
                                             catalog_row, model)

        self.logger.debug('loading catalog into plugin treeview')
        for catalog_id in self.catalog_plugins.catalog_ids():
            self._add_catalog_to_tree(catalog_id, store)

        catalog_cache = self.catalog_plugins.get_cache()
        for catalog_id in catalog_cache:
            if self.catalog_plugins.catalogs.get(catalog_id, None):
                continue
            named_catalog = catalog_cache[catalog_id]['value']
            model = (catalog_id, None, True, catalog_id, None, None, False,
                     False, False, _ROW_TYPE_CATALOG)
            catalog_row = gui_utilities.glib_idle_add_wait(
                self._store_append, store, None, model)
            for repo in named_catalog.repositories:
                model = (repo.id, None, True, repo.title, None, None, False,
                         False, False, _ROW_TYPE_REPOSITORY)
                repo_row = gui_utilities.glib_idle_add_wait(
                    self._store_append, store, catalog_row, model)
                self._add_plugins_offline(catalog_id, repo.id, store, repo_row)

        gui_utilities.glib_idle_add_once(self._treeview_unselect)
        self._update_status_bar('Loading completed', idle=True)

    def _add_catalog_to_tree(self, catalog_id, store):
        model = self._named_model(id=catalog_id,
                                  installed=None,
                                  enabled=True,
                                  title=catalog_id,
                                  compatibility=None,
                                  version=None,
                                  visible_enabled=False,
                                  visible_installed=False,
                                  sensitive_installed=False,
                                  type=_ROW_TYPE_CATALOG)
        catalog_row = gui_utilities.glib_idle_add_wait(self._store_append,
                                                       store, None, model)
        for repo in self.catalog_plugins.get_repositories(catalog_id):
            model = self._named_model(id=repo.id,
                                      installed=None,
                                      enabled=True,
                                      title=repo.title,
                                      compatibility=None,
                                      version=None,
                                      visible_enabled=False,
                                      visible_installed=False,
                                      sensitive_installed=False,
                                      type=_ROW_TYPE_REPOSITORY)
            repo_row = gui_utilities.glib_idle_add_wait(
                self._store_append, store, catalog_row, model)
            plugin_collections = self.catalog_plugins.get_collection(
                catalog_id, repo.id)
            if not plugin_collections:
                continue
            self._add_plugins_to_tree(catalog_id, repo, store, repo_row,
                                      plugin_collections)

    def _add_plugins_to_tree(self, catalog_id, repo, store, parent,
                             plugin_list):
        models = []
        for plugin_info in plugin_list.values():
            installed = False
            enabled = False
            plugin_name = plugin_info['name']
            install_src = self.config['plugins.installed'].get(plugin_name)
            if install_src and repo.id == install_src[
                    'repo_id'] and catalog_id == install_src['catalog_id']:
                installed = True
                enabled = plugin_name in self.config['plugins.enabled']
            models.append(
                self._named_model(
                    id=plugin_name,
                    installed=installed,
                    enabled=enabled,
                    title=plugin_info['title'],
                    compatibility='Yes' if self.catalog_plugins.is_compatible(
                        catalog_id, repo.id, plugin_name) else 'No',
                    version=plugin_info['version'],
                    visible_enabled=True,
                    visible_installed=True,
                    sensitive_installed=self.catalog_plugins.is_compatible(
                        catalog_id, repo.id, plugin_name),
                    type=_ROW_TYPE_PLUGIN))
        gui_utilities.glib_idle_add_once(self._store_extend, store, parent,
                                         models)

    def _add_plugins_offline(self, catalog_id, repo_id, store, parent):
        models = []
        for plugin_name, plugin_src in self.config['plugins.installed'].items(
        ):
            if plugin_src is None:
                continue
            if plugin_name not in self.application.plugin_manager:
                continue
            if plugin_src['catalog_id'] != catalog_id:
                continue
            if plugin_src['repo_id'] != repo_id:
                continue
            models.append(
                self._named_model(
                    id=plugin_name,
                    installed=True,
                    enabled=plugin_name in self.config['plugins.enabled'],
                    title=self.application.plugin_manager[plugin_name].title,
                    compatibility='Yes' if
                    self.application.plugin_manager[plugin_name].is_compatible
                    else 'No',
                    version=self.application.plugin_manager[plugin_name].
                    version,
                    visible_enabled=True,
                    visible_installed=True,
                    sensitive_installed=False,
                    type=_ROW_TYPE_PLUGIN))
        gui_utilities.glib_idle_add_once(self._store_extend, store, parent,
                                         models)

    def signal_popup_menu_activate_reload_all(self, _):
        if not self.load_thread.is_alive():
            self.load_thread = utilities.Thread(target=self._load_catalogs,
                                                kwargs={'refresh': True})
            self.load_thread.start()

    def signal_destory(self, _):
        pass

    def signal_treeview_row_activated(self, treeview, path, column):
        self._set_info(self._model[path])

    def signal_label_activate_link(self, _, uri):
        utilities.open_uri(uri)

    def signal_eventbox_button_press(self, widget, event):
        if not (event.type == Gdk.EventType.BUTTON_PRESS
                and event.button == Gdk.BUTTON_PRIMARY):
            return
        if not self._last_plugin_selected:
            return
        named_plugin = self._named_model(*self._last_plugin_selected)
        plugin_id = named_plugin.id
        if plugin_id is None:
            return
        if plugin_id in self.application.plugin_manager:
            klass = self.application.plugin_manager[plugin_id]
            compatibility_details = list(klass.compatibility)
        else:
            repo_model, catalog_model = self._get_plugin_model_parents(
                self._last_plugin_selected)
            compatibility_details = list(
                self.catalog_plugins.compatibility(catalog_model.id,
                                                   repo_model.id,
                                                   named_plugin.id))

        popover = Gtk.Popover()
        popover.set_relative_to(
            self.gobjects['label_plugin_info_for_compatible'])
        grid = Gtk.Grid()
        popover.add(grid)
        grid.insert_column(0)
        grid.insert_column(0)
        grid.insert_column(0)
        grid.set_column_spacing(3)

        compatibility_details.insert(0, ('Type', 'Value', 'Met'))
        row = 0
        for row, req in enumerate(compatibility_details):
            grid.insert_row(row)
            label = Gtk.Label(req[0])
            label.set_property('halign', Gtk.Align.START)
            grid.attach(label, 0, row, 1, 1)
            label = Gtk.Label(req[1])
            label.set_property('halign', Gtk.Align.START)
            grid.attach(label, 1, row, 1, 1)
            label = Gtk.Label(('Yes' if req[2] else 'No') if row else req[2])
            label.set_property('halign', Gtk.Align.END)
            grid.attach(label, 2, row, 1, 1)
        if not row:
            popover.destroy()
            return
        popover.show_all()

    def signal_expander_activate(self, expander):
        paned = self.gobjects['paned_plugins']
        if expander.get_property('expanded'):  # collapsing
            paned.set_position(paned.get_allocation().height +
                               self._paned_offset)

    def signal_paned_button_press_event(self, paned, event):
        return not self.gobjects['expander_info'].get_property('expanded')

    def signal_popup_menu_activate_reload(self, _):
        if not self.load_thread.is_alive():
            self.load_thread = utilities.Thread(target=self._reload)
            self.load_thread.start()

    def _reload(self):
        self._update_status_bar("Reloading... ")
        treeview = self.gobjects['treeview_plugins']
        pm = self.application.plugin_manager
        selected_plugin = None
        selection = treeview.get_selection()
        if selection.count_selected_rows():
            (model, tree_paths) = selection.get_selected_rows()
            selected_plugin = model[tree_paths[0]][0]

        for tree_iter in gui_utilities.gtk_treeview_selection_iterate(
                treeview):
            model_row = self._model[tree_iter]
            # only reloading installed plugins is currently supported
            named_row = self._named_model(*model_row)
            if named_row.type == _ROW_TYPE_CATALOG and named_row.id != _LOCAL_REPOSITORY_ID:
                self._reload_catalog(named_row, tree_iter)
            elif named_row.type == _ROW_TYPE_REPOSITORY:
                self._reload_repository(model_row)
            elif named_row.type == _ROW_TYPE_PLUGIN:
                if not named_row.installed:
                    self._update_status_bar(
                        'Cannot reload a plugin that is not installed.')
                    continue
                self._reload_plugin(named_row, model_row, pm, tree_iter,
                                    selected_plugin)
            else:
                self.logger.warning(
                    'reload selected for an unsupported row type')

    def _reload_plugin(self, named_row, model_row, pm, tree_iter,
                       selected_plugin):
        enabled = named_row.id in pm.enabled_plugins
        pm.unload(named_row.id)
        try:
            klass = pm.load(named_row.id, reload_module=True)
        except Exception as error:
            self._on_plugin_load_error(named_row.id, error)
            if named_row.id == selected_plugin:
                self._set_info(model_row)
            self._set_model_item(tree_iter, 'title',
                                 "{0} (Reload Failed)".format(named_row.id))
            return
        if named_row.id in self._module_errors:
            del self._module_errors[named_row.id]
        self._set_model_item(tree_iter, 'title', klass.title)
        self._set_model_item(tree_iter, 'compatibility',
                             'Yes' if klass.is_compatible else 'No')
        self._set_model_item(tree_iter, 'version', klass.version)
        if named_row.id == selected_plugin:
            self._set_info(self._model[tree_iter])
        if enabled:
            pm.enable(named_row.id)

    def _reload_catalog(self, named_row, tree_iter):
        self._model.remove(tree_iter)
        catalog_url = self.catalog_plugins.get_cache().get_catalog_by_id(
            named_row.id)['url']
        if not catalog_url:
            return
        catalog = self._load_catalog_from_url(catalog_url)
        if not catalog:
            return
        self.catalog_plugins.add_catalog(catalog,
                                         catalog_url=catalog_url,
                                         cache=True)
        self._load_plugins()

    def _reload_repository(self, model_row):
        parent_row = model_row.parent
        parent_named_row = self._named_model(*parent_row)
        if parent_named_row.type != _ROW_TYPE_CATALOG:
            self.logger.warning(
                'repository treeview row\'s parent is not a catalog')
            return
        return self._reload_catalog(parent_named_row, parent_row.iter)

    def signal_renderer_toggled_enable(self, _, path):
        pm = self.application.plugin_manager
        named_row = self._named_model(*self._model[path])
        if named_row.type != _ROW_TYPE_PLUGIN:
            return
        if named_row.id not in pm.loaded_plugins:
            return

        if named_row.id in self._module_errors:
            gui_utilities.show_dialog_error(
                'Can Not Enable Plugin', self.window,
                'Can not enable a plugin which failed to load.')
            return
        if named_row.enabled:
            self._disable_plugin(path)
        else:
            if not pm.loaded_plugins[named_row.id].is_compatible:
                gui_utilities.show_dialog_error(
                    'Incompatible Plugin', self.window,
                    'This plugin is not compatible.')
                return
            if not pm.enable(named_row.id):
                return
            self._set_model_item(path, 'enabled', True)
            self.config['plugins.enabled'].append(named_row.id)

    def signal_renderer_toggled_install(self, _, path):
        repo_model, catalog_model = self._get_plugin_model_parents(
            self._model[path])
        named_row = self._named_model(*self._model[path])
        if named_row.installed:
            self._update_status_bar("Uninstalling plugin {}...".format(
                named_row.id))
            if named_row.enabled:
                if not gui_utilities.show_dialog_yes_no(
                        'Plugin is Enabled', self.window,
                        'This will disable the plugin, do you want to continue?'
                ):
                    return
                self._disable_plugin(path)
            self._uninstall_plugin(path)
            self._update_status_bar("Uninstalling plugin {} completed.".format(
                named_row.id))
            return

        if named_row.id in self.config['plugins.installed']:
            plugin_src = self.config['plugins.installed'].get(named_row.id)
            if plugin_src != {
                    'catalog_id': catalog_model.id,
                    'repo_id': repo_model.id,
                    'plugin_id': named_row.id
            }:
                window_question = 'A plugin with this name is already installed from another\nrepository. Do you want to replace it with this one?'
                if not gui_utilities.show_dialog_yes_no(
                        'Plugin installed from another source', self.window,
                        window_question):
                    return
                if not self._remove_matching_plugin(path, plugin_src):
                    self.logger.warning(
                        "failed to uninstall plugin {0}".format(named_row.id))
                    return

        self._update_status_bar("Installing plugin {}...".format(
            named_row.title))
        try:
            self.catalog_plugins.install_plugin(catalog_model.id,
                                                repo_model.id, named_row.id,
                                                self.plugin_path)
        except requests.exceptions.ConnectionError:
            self.logger.warning("failed to download plugin {}".format(
                named_row.id))
            gui_utilities.show_dialog_error(
                'Failed To Install', self.window,
                "Failed to download {} plugin, check your internet connection."
                .format(named_row.id))
            return
        except Exception:
            self.logger.warning("failed to install plugin {}".format(
                named_row.id),
                                exc_info=True)
            gui_utilities.show_dialog_error(
                'Failed To Install', self.window,
                "Failed to install {} plugin.".format(named_row.id))
            self._update_status_bar("Installing plugin {} failed.".format(
                named_row.title))
            return

        self.config['plugins.installed'][named_row.id] = {
            'catalog_id': catalog_model.id,
            'repo_id': repo_model.id,
            'plugin_id': named_row.id
        }
        self._set_model_item(path, 'installed', True)
        self._set_model_item(
            path, 'version',
            self.catalog_plugins.get_collection(
                catalog_model.id, repo_model.id)[named_row.id]['version'])
        self.logger.info(
            "installed plugin {} from catalog:{}, repository:{}".format(
                named_row.id, catalog_model.id, repo_model.id))
        self._update_status_bar("Installing plugin {} completed.".format(
            named_row.title))
        self.application.plugin_manager.load_all(
            on_error=self._on_plugin_load_error)

    def _disable_plugin(self, path, is_path=True):
        named_row = self._named_model(
            *(self._model[path] if is_path else path))
        self.application.plugin_manager.disable(named_row.id)
        self.config['plugins.enabled'].remove(named_row.id)
        if is_path:
            self._set_model_item(path, 'enabled', False)
        else:
            path[self._named_model._fields.index('enabled')] = False

    def _remove_matching_plugin(self, path, plugin_src):
        named_row = self._named_model(*self._model[path])
        repo_model = None
        for catalog_model in self._model:
            catalog_id = self._named_model(*catalog_model).id
            if plugin_src and catalog_id == plugin_src['catalog_id']:
                repo_model = next(
                    (rm for rm in catalog_model.iterchildren()
                     if self._named_model(*rm).id == plugin_src['repo_id']),
                    None)
                break
            elif plugin_src is None and catalog_id == _LOCAL_REPOSITORY_ID:
                # local installation acts as a pseudo-repository
                repo_model = catalog_model
                break
        if not repo_model:
            return False
        for plugin_model in repo_model.iterchildren():
            named_model = self._named_model(*plugin_model)
            if named_model.id != named_row.id:
                continue
            if named_model.enabled:
                self._disable_plugin(plugin_model, is_path=False)
            self._uninstall_plugin(plugin_model.path)
            return True
        return False

    def _get_plugin_model_parents(self, plugin_model):
        return self._named_model(*plugin_model.parent), self._named_model(
            *plugin_model.parent.parent)

    def _uninstall_plugin(self, model_path):
        model_row = self._model[model_path]
        plugin_id = self._named_model(*model_row).id
        if os.path.isfile(
                os.path.join(self.plugin_path, plugin_id, '__init__.py')):
            shutil.rmtree(os.path.join(self.plugin_path, plugin_id))
        elif os.path.isfile(os.path.join(self.plugin_path, plugin_id + '.py')):
            os.remove(os.path.join(self.plugin_path, plugin_id + '.py'))
        else:
            self.logger.warning(
                "failed to find plugin {0} on disk for removal".format(
                    plugin_id))
            return False
        self.application.plugin_manager.unload(plugin_id)
        del self.config['plugins.installed'][plugin_id]

        if model_row.parent and model_row.parent[
                self._named_model._fields.index('id')] == _LOCAL_REPOSITORY_ID:
            del self._model[model_path]
        else:
            self._set_model_item(model_path, 'installed', False)
        self.logger.info(
            "successfully uninstalled plugin {0}".format(plugin_id))
        return True

    def _set_info(self, model_instance):
        named_model = self._named_model(*model_instance)
        stack = self.gobjects['stack_info']
        textview = self.gobjects['textview_plugin_info']
        buf = textview.get_buffer()
        buf.delete(buf.get_start_iter(), buf.get_end_iter())
        model_id = named_model.id
        if named_model.type == _ROW_TYPE_PLUGIN:
            if model_id in self._module_errors:
                stack.set_visible_child(textview)
                self._set_info_plugin_error(model_instance)
            else:
                stack.set_visible_child(self.gobjects['grid_plugin_info'])
                self._set_info_plugin(model_instance)
        else:
            self._set_info_nonplugin(model_instance)

    def _set_info_nonplugin(self, model_instance):
        stack = self.gobjects['stack_info']
        stack.set_visible_child(self.gobjects['grid_catalog_repo_info'])
        named_model = self._named_model(*model_instance)
        obj_catalog = None
        self._hide_catalog_repo_labels()
        self.gobjects['label_catalog_repo_info_title'].set_text(
            named_model.title)
        if not named_model.id:
            return
        if named_model.type == _ROW_TYPE_CATALOG:
            obj = self.catalog_plugins.catalogs.get(named_model.id, None)
            if not obj:
                return
        else:
            obj_catalog = self.catalog_plugins.catalogs.get(
                self._named_model(*model_instance.parent).id, None)
            if not obj_catalog:
                return
            obj = self.catalog_plugins.catalogs[self._named_model(
                *model_instance.parent).id].repositories[named_model.id]

        maintainers = getattr(obj, 'maintainers',
                              getattr(obj_catalog, 'maintainers', None))
        if maintainers:
            self.gobjects['label_catalog_repo_info_maintainers'].set_text(
                '\n'.join(maintainers))
            self.gobjects['label_catalog_repo_info_maintainers'].set_property(
                'visible', True)
            self.gobjects[
                'label_catalog_repo_info_for_maintainers'].set_property(
                    'visible', True)
        if getattr(obj, 'description', None):
            self.gobjects['label_catalog_repo_info_description'].set_text(
                obj.description)
            self.gobjects['label_catalog_repo_info_description'].set_property(
                'visible', True)
            self.gobjects[
                'label_catalog_repo_info_for_description'].set_property(
                    'visible', True)
        if getattr(obj, 'homepage', None) or getattr(obj, 'url', None):
            url = getattr(obj, 'homepage', getattr(obj, 'url', None))
            self.gobjects['label_catalog_repo_info_homepage'].set_markup(
                "<a href=\"{0}\">Homepage</a>".format(url))
            self.gobjects['label_catalog_repo_info_homepage'].set_property(
                'tooltip-text', url)
            self.gobjects['label_catalog_repo_info_homepage'].set_property(
                'visible', True)

    def _set_info_plugin(self, plugin_model):
        named_model = self._named_model(*plugin_model)
        pm = self.application.plugin_manager
        self._last_plugin_selected = plugin_model
        if named_model.id in pm.loaded_plugins:
            plugin = pm.loaded_plugins[named_model.id].metadata
            is_compatible = plugin['is_compatible']
        else:
            repo_model, catalog_model = self._get_plugin_model_parents(
                plugin_model)
            plugin = self.catalog_plugins.get_collection(
                catalog_model.id, repo_model.id)[named_model.id]
            is_compatible = self.catalog_plugins.is_compatible(
                catalog_model.id, repo_model.id, named_model.id)

        self.gobjects['label_plugin_info_title'].set_text(plugin['title'])
        self.gobjects['label_plugin_info_compatible'].set_text(
            'Yes' if is_compatible else 'No')
        self.gobjects['label_plugin_info_version'].set_text(plugin['version'])
        self.gobjects['label_plugin_info_authors'].set_text('\n'.join(
            plugin['authors']))
        self.gobjects['label_plugin_info_description'].set_text(
            plugin['description'])
        self._set_homepage_url(plugin['homepage'])

    def _set_info_plugin_error(self, model_instance):
        id_ = self._named_model(*model_instance).id
        textview = self.gobjects['textview_plugin_info']
        buf = textview.get_buffer()
        exc, formatted_exc = self._module_errors[id_]
        buf.insert(buf.get_end_iter(), "{0!r}\n\n".format(exc), -1)
        buf.insert(buf.get_end_iter(), ''.join(formatted_exc), -1)

    def _hide_catalog_repo_labels(self):
        self.gobjects['label_catalog_repo_info_maintainers'].set_property(
            'visible', False)
        self.gobjects['label_catalog_repo_info_for_maintainers'].set_property(
            'visible', False)
        self.gobjects['label_catalog_repo_info_description'].set_property(
            'visible', False)
        self.gobjects['label_catalog_repo_info_for_description'].set_property(
            'visible', False)
        self.gobjects['label_catalog_repo_info_homepage'].set_property(
            'visible', False)

    def _set_homepage_url(self, url=None):
        label_homepage = self.gobjects['label_plugin_info_homepage']
        if url is None:
            label_homepage.set_property('visible', False)
            return
        label_homepage.set_markup("<a href=\"{0}\">Homepage</a>".format(url))
        label_homepage.set_property('tooltip-text', url)
        label_homepage.set_property('visible', True)
Beispiel #17
0
class CampaignSelectionDialog(gui_utilities.GladeGObject):
    """
	Display a dialog which allows a new campaign to be created or an
	existing campaign to be opened.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('button_new_campaign', 'button_select',
                  'drawingarea_color_key', 'label_campaign_info',
                  'menubutton_filter', 'treeview_campaigns',
                  'revealer_loading', 'progressbar_loading'),
        top_level=('StockAddImage', ))
    view_columns = (extras.ColumnDefinitionString('Campaign Name'),
                    extras.ColumnDefinitionString('Company'),
                    extras.ColumnDefinitionString('Type'),
                    extras.ColumnDefinitionInteger('Messages'),
                    extras.ColumnDefinitionString('Created By'),
                    extras.ColumnDefinitionDatetime('Creation Date'),
                    extras.ColumnDefinitionDatetime('Expiration'))
    top_gobject = 'dialog'

    def __init__(self, *args, **kwargs):
        super(CampaignSelectionDialog, self).__init__(*args, **kwargs)
        treeview = self.gobjects['treeview_campaigns']
        self.treeview_manager = managers.TreeViewManager(
            treeview,
            cb_delete=self._prompt_to_delete_row,
            cb_refresh=self.load_campaigns)
        self.treeview_manager.set_column_titles(
            tuple(column.title for column in self.view_columns),
            column_offset=4,
            renderers=tuple(column.cell_renderer()
                            for column in self.view_columns))
        treeview.set_tooltip_column(1)
        self.treeview_manager.set_column_color(background=2, foreground=3)
        self.popup_menu = self.treeview_manager.get_popup_menu()
        self._creation_assistant = None

        view_column_types = tuple(column.g_type
                                  for column in self.view_columns)
        self._tv_model = Gtk.ListStore(str, str, Gdk.RGBA, Gdk.RGBA,
                                       *view_column_types)

        # create and set the filter for expired campaigns
        self._tv_model_filter = self._tv_model.filter_new()
        self._tv_model_filter.set_visible_func(self._filter_campaigns)
        tree_model_sort = Gtk.TreeModelSort(model=self._tv_model_filter)
        for idx, column in enumerate(self.view_columns, 4):
            if column.sort_function is not None:
                tree_model_sort.set_sort_func(idx, column.sort_function, idx)
        # default sort is descending by campaign creation date
        tree_model_sort.set_sort_column_id(9, Gtk.SortType.DESCENDING)
        treeview.set_model(tree_model_sort)

        self.destroyed = False
        self.dialog.connect('destroy', self.signal_dialog_destroy)
        # setup menus for filtering campaigns and load campaigns
        self.get_popup_filter_menu()
        self.load_campaigns()

    def get_popup_filter_menu(self):
        # create filter menu and menuitems
        filter_menu = Gtk.Menu()
        menu_item_expired = Gtk.CheckMenuItem('Expired campaigns')
        menu_item_user = Gtk.CheckMenuItem('Your campaigns')
        menu_item_other = Gtk.CheckMenuItem('Other campaigns')
        self.filter_menu_items = {
            'expired_campaigns': menu_item_expired,
            'your_campaigns': menu_item_user,
            'other_campaigns': menu_item_other
        }
        # set up the menuitems and add it to the menubutton
        for menus in self.filter_menu_items:
            filter_menu.append(self.filter_menu_items[menus])
            self.filter_menu_items[menus].connect(
                'toggled', self.signal_checkbutton_toggled)
            self.filter_menu_items[menus].show()
        self.filter_menu_items['expired_campaigns'].set_active(
            self.config['filter.campaign.expired'])
        self.filter_menu_items['your_campaigns'].set_active(
            self.config['filter.campaign.user'])
        self.filter_menu_items['other_campaigns'].set_active(
            self.config['filter.campaign.other_users'])
        self.gobjects['menubutton_filter'].set_popup(filter_menu)
        filter_menu.connect('destroy', self._save_filter)

    def _save_filter(self, _):
        self.config['filter.campaign.expired'] = self.filter_menu_items[
            'expired_campaigns'].get_active()
        self.config['filter.campaign.user'] = self.filter_menu_items[
            'your_campaigns'].get_active()
        self.config['filter.campaign.other_users'] = self.filter_menu_items[
            'other_campaigns'].get_active()

    def _filter_campaigns(self, model, tree_iter, _):
        named_row = _ModelNamedRow(*model[tree_iter])
        username = self.config['server_username']
        if not self.filter_menu_items['your_campaigns'].get_active():
            if username == named_row.user:
                return False
        if not self.filter_menu_items['other_campaigns'].get_active():
            if username != named_row.user:
                return False
        if named_row.expiration is None:
            return True
        if named_row.expiration < datetime.datetime.now():
            if not self.filter_menu_items['expired_campaigns'].get_active():
                return False
        return True

    def _highlight_campaign(self, campaign_name):
        treeview = self.gobjects['treeview_campaigns']
        model = treeview.get_model()
        model_iter = gui_utilities.gtk_list_store_search(
            model, campaign_name, column=_ModelNamedRow._fields.index('name'))
        if model_iter:
            treeview.set_cursor(model.get_path(model_iter), None, False)
            return True
        return False

    def _prompt_to_delete_row(self, treeview, selection):
        (model, tree_iter) = selection.get_selected()
        if not tree_iter:
            return
        campaign_id = model.get_value(tree_iter, 0)
        if self.config.get('campaign_id') == campaign_id:
            gui_utilities.show_dialog_warning(
                'Can Not Delete Campaign', self.dialog,
                'Can not delete the current campaign.')
            return
        if not gui_utilities.show_dialog_yes_no(
                'Delete This Campaign?', self.dialog,
                'This action is irreversible, all campaign data will be lost.'
        ):
            return
        self.application.emit('campaign-delete', campaign_id)
        self.load_campaigns()
        self._highlight_campaign(self.config.get('campaign_name'))

    def load_campaigns(self, cursor=None):
        """Load campaigns from the remote server and populate the :py:class:`Gtk.TreeView`."""
        if cursor is None:
            self._tv_model.clear()
            self.gobjects['revealer_loading'].set_reveal_child(True)
            self.gobjects['progressbar_loading'].set_fraction(0.0)
        path = find.data_file(os.path.join('queries', 'get_campaigns.graphql'))
        if path is None:
            raise errors.KingPhisherResourceError(
                'could not find GraphQL query file: get_campaigns.graphql')
        self.application.rpc.async_graphql_file(
            path,
            query_vars={
                'cursor': cursor,
                'page': _QUERY_PAGE_SIZE
            },
            on_success=self.__async_rpc_cb_load_campaigns,
            when_idle=True)

    def __async_rpc_cb_load_campaigns(self, results):
        if self.destroyed:
            return
        store = self._tv_model
        style_context = self.dialog.get_style_context()
        bg_color = gui_utilities.gtk_style_context_get_color(
            style_context, 'theme_color_tv_bg', default=ColorHexCode.WHITE)
        fg_color = gui_utilities.gtk_style_context_get_color(
            style_context, 'theme_color_tv_fg', default=ColorHexCode.BLACK)
        hlbg_color = gui_utilities.gtk_style_context_get_color(
            style_context,
            'theme_color_tv_hlbg',
            default=ColorHexCode.LIGHT_YELLOW)
        hlfg_color = gui_utilities.gtk_style_context_get_color(
            style_context, 'theme_color_tv_hlfg', default=ColorHexCode.BLACK)
        now = datetime.datetime.now()
        campaigns = results['db']['campaigns']
        for campaign in campaigns['edges']:
            campaign = campaign['node']
            created_ts = utilities.datetime_utc_to_local(campaign['created'])
            expiration_ts = campaign['expiration']
            is_expired = False
            if expiration_ts is not None:
                expiration_ts = utilities.datetime_utc_to_local(expiration_ts)
                is_expired = expiration_ts < now
            store.append(
                _ModelNamedRow(
                    id=str(campaign['id']),
                    description=(html.escape(campaign['description'],
                                             quote=True)
                                 if campaign['description'] else None),
                    bg_color=(hlbg_color if is_expired else bg_color),
                    fg_color=(hlfg_color if is_expired else fg_color),
                    name=campaign['name'],
                    company=(campaign['company']['name']
                             if campaign['company'] is not None else None),
                    type=(campaign['campaignType']['name']
                          if campaign['campaignType'] is not None else None),
                    messages=campaign['messages']['total'],
                    user=campaign['user']['name'],
                    created=created_ts,
                    expiration=expiration_ts,
                ))
        self.gobjects['label_campaign_info'].set_text(
            "Showing {0} of {1:,} Campaign{2}".format(
                len(self._tv_model_filter), len(self._tv_model),
                ('' if len(self._tv_model) == 1 else 's')))
        if campaigns['total']:
            progress = len(self._tv_model) / campaigns['total']
        else:
            progress = 1.0
        self.gobjects['progressbar_loading'].set_text(
            "Loaded {:,} of {:,} campaigns ({:.1f}%)".format(
                len(self._tv_model), campaigns['total'], progress * 100))
        self.gobjects['progressbar_loading'].set_fraction(progress)
        if campaigns['pageInfo']['hasNextPage']:
            self.load_campaigns(cursor=campaigns['pageInfo']['endCursor'])
        else:
            self.gobjects['revealer_loading'].set_reveal_child(False)
            self.gobjects['progressbar_loading'].set_fraction(1.0)

    def signal_assistant_destroy(self, _, campaign_creation_assistant):
        self._creation_assistant = None
        campaign_name = campaign_creation_assistant.campaign_name
        if not campaign_name:
            return
        self.load_campaigns()
        self._highlight_campaign(campaign_name)
        self.dialog.response(Gtk.ResponseType.APPLY)

    def signal_dialog_destroy(self, _):
        self.destroyed = True

    def signal_button_clicked(self, button):
        if self._creation_assistant is not None:
            gui_utilities.show_dialog_warning(
                'Campaign Creation Assistant', self.dialog,
                'The campaign creation assistant is already active.')
            return
        assistant = CampaignAssistant(self.application)
        assistant.assistant.set_transient_for(self.dialog)
        assistant.assistant.set_modal(True)
        assistant.assistant.connect('destroy', self.signal_assistant_destroy,
                                    assistant)
        assistant.interact()
        self._creation_assistant = assistant

    def signal_checkbutton_toggled(self, _):
        self._tv_model_filter.refilter()
        self.gobjects['label_campaign_info'].set_text(
            "Showing {0:,} of {1:,} Campaign{2}".format(
                len(self._tv_model_filter), len(self._tv_model),
                ('' if len(self._tv_model) == 1 else 's')))

    def signal_drawingarea_draw(self, drawingarea, context):
        width, height = drawingarea.get_size_request()
        context.rectangle(0, 0, width, height)
        context.stroke_preserve()
        style_context = self.dialog.get_style_context()
        hlbg_color = gui_utilities.gtk_style_context_get_color(
            style_context,
            'theme_color_tv_hlbg',
            default=ColorHexCode.LIGHT_YELLOW)
        context.set_source_rgb(hlbg_color.red, hlbg_color.green,
                               hlbg_color.blue)
        context.fill()

    def signal_treeview_row_activated(self, treeview, treeview_column,
                                      treepath):
        self.gobjects['button_select'].emit('clicked')

    def interact(self):
        self._highlight_campaign(self.config.get('campaign_name'))
        self.dialog.show_all()
        response = self.dialog.run()
        old_campaign_id = self.config.get('campaign_id')
        while response != Gtk.ResponseType.CANCEL:
            treeview = self.gobjects['treeview_campaigns']
            selection = treeview.get_selection()
            (model, tree_iter) = selection.get_selected()
            if tree_iter:
                break
            gui_utilities.show_dialog_error(
                'No Campaign Selected', self.dialog,
                'Either select a campaign or create a new one.')
            response = self.dialog.run()
        if response == Gtk.ResponseType.APPLY:
            named_row = _ModelNamedRow(*model[tree_iter])
            if old_campaign_id is None or (
                    old_campaign_id is not None
                    and named_row.id != str(old_campaign_id)):
                self.config['campaign_id'] = named_row.id
                self.config['campaign_name'] = named_row.name
                self.application.emit('campaign-set', old_campaign_id,
                                      named_row.id)
        self.dialog.destroy()
        return response
Beispiel #18
0
class MailSenderSendTab(gui_utilities.GladeGObject):
	"""
	This allows the :py:class:`.MailSenderThread` object to be managed
	by the user through the GUI. These two classes are very interdependent
	"""
	dependencies = gui_utilities.GladeDependencies(
		children=(
			'button_mail_sender_start',
			'button_mail_sender_stop',
			'textview_mail_sender_progress',
			'togglebutton_mail_sender_pause',
			'progressbar_mail_sender',
			'scrolledwindow_mail_sender_progress'
		),
		top_level=('StockMediaPlayImage',)
	)
	top_gobject = 'box'
	def __init__(self, *args, **kwargs):
		self.label = Gtk.Label(label='Send')
		"""The :py:class:`Gtk.Label` representing this tabs name."""
		super(MailSenderSendTab, self).__init__(*args, **kwargs)
		self.textview = self.gobjects['textview_mail_sender_progress']
		"""The :py:class:`Gtk.TextView` object that renders text status messages."""
		self.textview.modify_font(Pango.FontDescription(self.config['text_font']))
		self.textbuffer = self.textview.get_buffer()
		"""The :py:class:`Gtk.TextBuffer` instance associated with :py:attr:`~.MailSenderSendTab.textview`."""
		self.textbuffer_iter = self.textbuffer.get_start_iter()
		self.progressbar = self.gobjects['progressbar_mail_sender']
		"""The :py:class:`Gtk.ProgressBar` instance which is used to display progress of sending messages."""
		self.pause_button = self.gobjects['togglebutton_mail_sender_pause']
		self.sender_thread = None
		"""The :py:class:`.MailSenderThread` instance that is being used to send messages."""
		self.application.connect('exit', self.signal_kpc_exit)
		self.application.connect('exit-confirm', self.signal_kpc_exit_confirm)
		self.textview.connect('populate-popup', self.signal_textview_populate_popup)

	def _sender_precheck_attachment(self):
		attachment = self.config.get('mailer.attachment_file')
		if not attachment:
			return True
		self.text_insert("File '{0}' will be attached to sent messages.\n".format(os.path.basename(attachment)))
		_, extension = os.path.splitext(attachment)
		extension = extension[1:]
		if self.config['remove_attachment_metadata'] and extension in ('docm', 'docx', 'pptm', 'pptx', 'xlsm', 'xlsx'):
			scrubber.remove_office_metadata(attachment)
			self.text_insert("Attachment file detected as MS Office 2007+, metadata has been removed.\n")
		md5 = hashlib.new('md5')
		sha1 = hashlib.new('sha1')
		with open(attachment, 'rb') as file_h:
			data = True
			while data:
				data = file_h.read(1024)
				md5.update(data)
				sha1.update(data)
		self.text_insert("  MD5:  {0}\n".format(md5.hexdigest()))
		self.text_insert("  SHA1: {0}\n".format(sha1.hexdigest()))
		return True

	def _sender_precheck_campaign(self):
		campaign = self.application.rpc.remote_table_row('campaigns', self.config['campaign_id'])
		if campaign.expiration and campaign.expiration < datetime.datetime.utcnow():
			gui_utilities.show_dialog_warning('Campaign Is Expired', self.parent, 'The current campaign has already expired')
			return False
		return True

	def _sender_precheck_settings(self):
		required_settings = {
			'mailer.webserver_url': 'Web Server URL',
			'mailer.subject': 'Subject',
			'mailer.html_file': 'Message HTML File'
		}
		target_type = self.config.get('mailer.target_type')
		if target_type == 'file':
			required_settings['mailer.target_file'] = 'Target CSV File'
		elif target_type == 'single':
			required_settings['mailer.target_email_address'] = 'Target Email Address'
			required_settings['mailer.target_name'] = 'Target Name'
		else:
			gui_utilities.show_dialog_warning('Invalid Target Type', self.parent, 'Please specify a target file or name and email address.')
			return False
		message_type = self.config.get('mailer.message_type')
		if not message_type in ('email', 'calendar_invite'):
			gui_utilities.show_dialog_warning('Invalid Message Type', self.parent, 'Please select a valid message type.')
			return False
		for setting, setting_name in required_settings.items():
			if not self.config.get(setting):
				gui_utilities.show_dialog_warning("Missing Required Option: '{0}'".format(setting_name), self.parent, 'Return to the Config tab and set all required options')
				return
			if not setting.endswith('_file'):
				continue
			file_path = self.config[setting]
			if not (os.path.isfile(file_path) and os.access(file_path, os.R_OK)):
				gui_utilities.show_dialog_warning('Invalid Option Configuration', self.parent, "Setting: '{0}'\nReason: the file could not be read.".format(setting_name))
				return False
		if not self.config.get('smtp_server'):
			gui_utilities.show_dialog_warning('Missing SMTP Server Setting', self.parent, 'Please configure the SMTP server')
			return False
		return True

	def _sender_precheck_source(self):
		valid = utilities.is_valid_email_address(self.config['mailer.source_email'])
		valid = valid and utilities.is_valid_email_address(self.config['mailer.source_email_smtp'])
		if valid:
			return True
		self.text_insert('WARNING: One or more source email addresses specified are invalid.\n')
		if not gui_utilities.show_dialog_yes_no('Invalid Email Address', self.parent, 'One or more source email addresses specified are invalid.\nContinue sending messages anyways?'):
			self.text_insert('Sending aborted due to invalid source email address.\n')
			return False
		return True

	def _sender_precheck_spf(self):
		spf_check_level = self.config['spf_check_level']
		if not spf_check_level:
			return True
		if not utilities.is_valid_email_address(self.config['mailer.source_email_smtp']):
			self.text_insert('WARNING: Can not check SPF records for an invalid source email address.\n')
			return True

		spf_test_ip = mailer.guess_smtp_server_address(self.config['smtp_server'], (self.config['ssh_server'] if self.config['smtp_ssh_enable'] else None))
		if not spf_test_ip:
			self.text_insert('Skipped checking the SPF policy because the SMTP server address could not be detected.\n')
			self.logger.warning('skipping spf policy check because the smtp server address could not be reliably detected')
			return True

		self.logger.debug('detected the smtp server address as ' + str(spf_test_ip))
		spf_test_sender, spf_test_domain = self.config['mailer.source_email_smtp'].split('@')
		self.text_insert("Checking the SPF policy of target domain '{0}'... ".format(spf_test_domain))
		try:
			spf_test = spf.SenderPolicyFramework(spf_test_ip, spf_test_domain, spf_test_sender)
			spf_result = spf_test.check_host()
		except spf.SPFError as error:
			self.text_insert("done, encountered exception: {0}.\n".format(error.__class__.__name__))
			return True

		if not spf_result:
			self.text_insert('done, no policy was found.\n')
		else:
			self.text_insert('done.\n')
		dialog_title = 'Sender Policy Framework Failure'
		dialog_message = None
		if spf_check_level == 1 and spf_result in [SPFResult.FAIL, SPFResult.SOFT_FAIL]:
			dialog_message = 'The configuration fails the domains SPF policy.\nMessages may be marked as forged.'
		elif spf_check_level == 2 and not spf_result in [SPFResult.NEUTRAL, SPFResult.PASS]:
			dialog_message = 'The configuration does not pass the domains SPF policy.'
		spf_result = spf_result or 'N/A (No policy found)'
		self.text_insert("{0}SPF policy result: {1}\n".format(('WARNING: ' if spf_result.endswith('fail') else ''), spf_result))
		if dialog_message:
			dialog_message += '\n\nContinue sending messages anyways?'
			if not gui_utilities.show_dialog_yes_no(dialog_title, self.parent, dialog_message):
				self.text_insert('Sending aborted due to the SPF policy.\n')
				return False
		return True

	def _sender_precheck_url(self):
		self.text_insert('Checking the target URL... ')
		try:
			response = test_webserver_url(self.config['mailer.webserver_url'], self.config['server_config']['server.secret_id'])
			assert response.ok
		except (AssertionError, requests.exceptions.ConnectionError, requests.exceptions.RequestException):
			self.text_insert('failed')
			if not gui_utilities.show_dialog_yes_no('Unable To Open The Web Server URL', self.parent, 'The URL may be invalid, continue sending messages anyways?'):
				self.text_insert(', sending aborted.\n')
				return
			self.text_insert(', error ignored.\n')
		else:
			self.text_insert('success, done.\n')
		return True

	def signal_activate_popup_menu_clear_all(self, widget):
		self.textbuffer.delete(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter())
		self.textbuffer_iter = self.textbuffer.get_start_iter()

	def signal_button_clicked_sender_start(self, button):
		if not self._sender_precheck_settings():
			return
		if not self._sender_precheck_campaign():
			return
		if not self._sender_precheck_url():
			return
		if not self._sender_precheck_source():
			return
		if not self._sender_precheck_spf():
			return
		if not self._sender_precheck_attachment():
			return
		mailer_tab = self.application.main_tabs['mailer']
		if not all(mailer_tab.emit('send-precheck')):
			self.text_insert('Message pre-check conditions failed, aborting.\n')
			return

		self.text_insert("Sending messages started at: {:%A %B %d, %Y %H:%M:%S}\n".format(datetime.datetime.now()))
		self.text_insert("Message mode is: {0}\n".format(self.config['mailer.message_type'].replace('_', ' ').title()))

		# after this the operation needs to call self.sender_start_failure to quit
		if self.sender_thread:
			return
		self.application.emit('config-save')
		self.gobjects['button_mail_sender_start'].set_sensitive(False)
		self.gobjects['button_mail_sender_stop'].set_sensitive(True)
		self.progressbar.set_fraction(0)
		self.sender_thread = mailer.MailSenderThread(self.application, self.config['mailer.target_file'], self.application.rpc, self)

		# verify settings
		missing_files = self.sender_thread.missing_files()
		if missing_files:
			text = ''.join("Missing required file: '{0}'\n".format(f) for f in missing_files)
			self.sender_start_failure('Missing required files', text)
			return

		# connect to the smtp server
		if self.config['smtp_ssh_enable']:
			while True:
				self.text_insert('Connecting to SSH... ')
				login_dialog = dialogs.SSHLoginDialog(self.application)
				response = login_dialog.interact()
				if response != Gtk.ResponseType.APPLY:
					self.sender_start_failure(text='canceled.\n')
					return
				connection_status = self.sender_thread.server_ssh_connect()
				if connection_status == ConnectionErrorReason.SUCCESS:
					self.text_insert('done.\n')
					break
				if connection_status == ConnectionErrorReason.ERROR_AUTHENTICATION_FAILED:
					error_description = ('Authentication Failed', 'Failed to authenticate to the SSH server.')
				else:
					error_description = ('Connection Failed', 'Failed to connect to the SSH server.')
				self.sender_start_failure(error_description, 'failed.\n', retry=True)

		self.text_insert('Connecting to SMTP server... ')
		if self.config.get('smtp_username', ''):
			login_dialog = dialogs.SMTPLoginDialog(self.application)
			response = login_dialog.interact()
			if response != Gtk.ResponseType.APPLY:
				self.sender_start_failure(text='canceled.\n')
				return
		connection_status = self.sender_thread.server_smtp_connect()
		if connection_status == ConnectionErrorReason.ERROR_AUTHENTICATION_FAILED:
			self.sender_start_failure(('Authentication Failed', 'Failed to authenticate to the SMTP server.'), 'failed.\n')
			return
		elif connection_status != ConnectionErrorReason.SUCCESS:
			self.sender_start_failure(('Connection Failed', 'Failed to connect to the SMTP server.'), 'failed.\n')
			return
		self.text_insert('done.\n')

		parsed_target_url = urllib.parse.urlparse(self.config['mailer.webserver_url'])
		landing_page_hostname = parsed_target_url.netloc
		landing_page = parsed_target_url.path
		landing_page = landing_page.lstrip('/')
		self.application.rpc('campaign/landing_page/new', self.config['campaign_id'], landing_page_hostname, landing_page)

		self.sender_thread.start()
		self.gobjects['togglebutton_mail_sender_pause'].set_sensitive(True)

	def signal_button_clicked_sender_stop(self, button):
		if not self.sender_thread:
			return
		if not gui_utilities.show_dialog_yes_no('King Phisher Is Sending Messages', self.parent, 'Are you sure you want to stop?'):
			return
		self.sender_thread.stop()
		self.gobjects['button_mail_sender_stop'].set_sensitive(False)
		self.gobjects['button_mail_sender_start'].set_sensitive(True)
		self.gobjects['togglebutton_mail_sender_pause'].set_property('active', False)
		self.gobjects['togglebutton_mail_sender_pause'].set_sensitive(False)

	def signal_button_toggled_sender_pause(self, button):
		if not self.sender_thread:
			return
		if button.get_property('active'):
			self.sender_thread.pause()
		else:
			self.sender_thread.unpause()

	def signal_kpc_exit(self, kpc):
		if self.sender_thread and self.sender_thread.is_alive():
			self.logger.info('stopping the sender thread because the client is exiting')
			self.sender_thread.stop()

	def signal_kpc_exit_confirm(self, kpc):
		if not self.sender_thread:
			return
		if not self.sender_thread.is_alive():
			return
		if gui_utilities.show_dialog_yes_no('King Phisher Is Sending Messages', self.parent, 'Are you sure you want to exit?'):
			return
		kpc.emit_stop_by_name('exit-confirm')

	def signal_textview_populate_popup(self, textview, menu):
		menu_item = Gtk.MenuItem.new_with_label('Clear All')
		menu_item.connect('activate', self.signal_activate_popup_menu_clear_all)
		menu_item.show()
		menu.append(menu_item)
		return True

	def signal_textview_size_allocate_autoscroll(self, textview, allocation):
		scrolled_window = self.gobjects['scrolledwindow_mail_sender_progress']
		adjustment = scrolled_window.get_vadjustment()
		adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size())

	def text_insert(self, message):
		"""
		Insert text into the :py:attr:`~.MailSenderSendTab.textbuffer`.

		:param str message: The text to insert.
		"""
		self.textbuffer.insert(self.textbuffer_iter, message)

	def notify_status(self, message):
		"""
		A call back use by :py:class:`.MailSenderThread` to update
		general status information.

		:param str message: The status message.
		"""
		self.text_insert(message)

	def notify_sent(self, emails_done, emails_total):
		"""
		A call back use by :py:class:`.MailSenderThread` to notify when
		an email has been successfully sent to the SMTP server.

		:param int emails_done: The number of email messages that have been sent.
		:param int emails_total: The total number of email messages that need to be sent.
		"""
		self.progressbar.set_fraction(float(emails_done) / float(emails_total))

	def sender_start_failure(self, message=None, text=None, retry=False):
		"""
		Handle a failure in starting the message sender thread and
		perform any necessary clean up.

		:param message: A message to shown in an error popup dialog.
		:type message: str, tuple
		:param text message: A message to be inserted into the text buffer.
		:param bool retry: The operation will be attempted again.
		"""
		if text:
			self.text_insert(text)
		self.gobjects['button_mail_sender_stop'].set_sensitive(False)
		self.gobjects['button_mail_sender_start'].set_sensitive(True)
		if isinstance(message, str):
			gui_utilities.show_dialog_error(message, self.parent)
		elif isinstance(message, tuple) and len(message) == 2:
			gui_utilities.show_dialog_error(message[0], self.parent, message[1])
		if not retry:
			self.sender_thread = None

	def notify_stopped(self):
		"""
		A callback used by :py:class:`.MailSenderThread` to notify when the
		thread has stopped.
		"""
		self.progressbar.set_fraction(1)
		self.gobjects['button_mail_sender_stop'].set_sensitive(False)
		self.gobjects['togglebutton_mail_sender_pause'].set_property('active', False)
		self.gobjects['togglebutton_mail_sender_pause'].set_sensitive(False)
		self.gobjects['button_mail_sender_start'].set_sensitive(True)
		self.sender_thread = None
		self.application.main_tabs['mailer'].emit('send-finished')
Beispiel #19
0
class CampaignAssistant(gui_utilities.GladeGObject):
    """
	Display an assistant which walks the user through creating a new campaign or
	configuring an existing campaign. If no *campaign_id* is specified a new
	campaign will be created.
	"""
    dependencies = gui_utilities.GladeDependencies(children=(
        resources.CompanyEditorGrid(
            gui_utilities.GladeProxyDestination('add',
                                                widget='alignment_company')),
        'button_select_kpm_dest_folder',
        'button_select_kpm_file',
        'button_url_ssl_issue_certificate',
        'calendar_campaign_expiration',
        'checkbutton_alert_subscribe',
        'checkbutton_expire_campaign',
        'checkbutton_reject_after_credentials',
        'combobox_campaign_type',
        'combobox_company_existing',
        'combobox_url_hostname',
        'combobox_url_path',
        'combobox_url_scheme',
        'entry_campaign_description',
        'entry_campaign_name',
        'entry_kpm_dest_folder',
        'entry_kpm_file',
        'entry_test_validation_text',
        'entry_validation_regex_mfa_token',
        'entry_validation_regex_password',
        'entry_validation_regex_username',
        'frame_campaign_expiration',
        'frame_company_existing',
        'frame_company_new',
        'image_intro_title',
        'image_url_ssl_status',
        'label_confirm_body',
        'label_confirm_title',
        'label_intro_body',
        'label_intro_title',
        'label_url_for_scheme',
        'label_url_for_hostname',
        'label_url_for_path',
        'label_url_info_authors',
        'label_url_info_created',
        'label_url_info_description',
        'label_url_info_title',
        'label_url_info_for_authors',
        'label_url_preview',
        'label_url_ssl_for_status',
        'label_url_ssl_status',
        'label_validation_regex_mfa_token',
        'label_validation_regex_password',
        'label_validation_regex_username',
        'listbox_url_info_classifiers',
        'listbox_url_info_references',
        'radiobutton_company_existing',
        'radiobutton_company_new',
        'radiobutton_company_none',
        'revealer_url_ssl_settings',
        'togglebutton_expiration_time',
    ),
                                                   top_level=(
                                                       'ClockHourAdjustment',
                                                       'ClockMinuteAdjustment',
                                                       'StockExecuteImage',
                                                   ))
    top_gobject = 'assistant'
    objects_persist = False

    def __init__(self, application, campaign_id=None):
        """
		:param application: The application instance which this object belongs to.
		:type application: :py:class:`~king_phisher.client.application.KingPhisherClientApplication`
		:param campaign_id: The ID of the campaign to edit.
		"""
        super(CampaignAssistant, self).__init__(application)
        self.campaign_id = campaign_id
        self._close_ready = True
        self._page_titles = {}
        for page_n in range(self.assistant.get_n_pages()):
            page = self.assistant.get_nth_page(page_n)
            page_title = self.assistant.get_page_title(page)
            if page_title:
                self._page_titles[page_title] = page_n

        campaign_edges = self.application.rpc.graphql(
            '{ db { campaigns { edges { node { id name } } } } }',
        )['db']['campaigns']['edges']
        self._campaign_names = dict((edge['node']['name'], edge['node']['id'])
                                    for edge in campaign_edges)
        self._cache_hostname = {}
        self._cache_site_template = {}
        self._can_issue_certs = False
        self._ssl_status = {}

        self._expiration_time = managers.TimeSelectorButtonManager(
            self.application, self.gobjects['togglebutton_expiration_time'])
        self._set_comboboxes()
        self._set_defaults()
        self.application.rpc.async_graphql(
            '{ ssl { status { enabled hasLetsencrypt hasSni } } }',
            on_success=self.__async_rpc_cb_ssl_status)
        _homogenous_label_width((self.gobjects['label_url_for_scheme'],
                                 self.gobjects['label_url_ssl_for_status'],
                                 self.gobjects['label_url_info_for_authors']))

        if not self.config['server_config']['server.require_id']:
            self.gobjects[
                'checkbutton_reject_after_credentials'].set_sensitive(False)
            self.gobjects['checkbutton_reject_after_credentials'].set_property(
                'active', False)

        confirm_preamble = 'Verify all settings are correct in the previous sections'
        if campaign_id:
            # re-configuring an existing campaign
            self.gobjects['label_confirm_body'].set_text(
                confirm_preamble +
                ', then hit "Apply" to update the King Phisher campaign with the new settings.'
            )
            self.gobjects['label_intro_body'].set_text(
                'This assistant will walk you through reconfiguring the selected King Phisher campaign.'
            )
            self.gobjects['label_intro_title'].set_text('Configure Campaign')
            self._set_webserver_url(self.config['mailer.webserver_url'])
        else:
            # creating a new campaign
            self.gobjects['label_confirm_body'].set_text(
                confirm_preamble +
                ', then hit "Apply" to create the new King Phisher campaign.')
            self.gobjects['label_intro_body'].set_text(
                'This assistant will walk you through creating and configuring a new King Phisher campaign.'
            )
            self.gobjects['label_intro_title'].set_text('New Campaign')

    def __async_rpc_cb_issue_cert_error(self, error, message=None):
        self._set_page_complete(True, page='Web Server URL')
        self.gobjects['button_url_ssl_issue_certificate'].set_sensitive(True)
        _set_icon(self.gobjects['image_url_ssl_status'], 'gtk-dialog-warning')
        label = self.gobjects['label_url_ssl_status']
        label.set_text(
            'An error occurred while requesting a certificate for the specified hostname'
        )
        gui_utilities.show_dialog_error(
            'Operation Error', self.application.get_active_window(), message
            or "Unknown error: {!r}".format(error))

    def __async_rpc_cb_issue_cert_success(self, result):
        self._set_page_complete(True, page='Web Server URL')
        if not result['success']:
            return self.__async_rpc_cb_issue_cert_error(
                None, result['message'])
        _set_icon(self.gobjects['image_url_ssl_status'], 'gtk-yes')
        label = self.gobjects['label_url_ssl_status']
        label.set_text(
            'A certificate for the specified hostname has been issued and loaded'
        )

    def __async_rpc_cb_populate_url_hostname_combobox(self, hostnames):
        hostnames = sorted(hostnames)
        model = self.gobjects['combobox_url_hostname'].get_model()
        for hostname in hostnames:
            model.append((hostname, ))

    def __async_rpc_cb_populate_url_info(self, hostname, path, results):
        self._cache_site_template[(hostname, path)] = results
        template = results['siteTemplate']
        if template is None:
            return
        self.gobjects['label_url_info_created'].set_text(
            utilities.format_datetime(
                utilities.datetime_utc_to_local(template['created'])))
        metadata = template['metadata']
        if metadata is None:
            return
        self.gobjects['label_url_info_title'].set_text(metadata['title'])
        self.gobjects['label_url_info_authors'].set_text('\n'.join(
            metadata['authors']))
        self.gobjects['label_url_info_description'].set_text(
            metadata['description'])
        if metadata['referenceUrls']:
            gui_utilities.gtk_listbox_populate_urls(
                self.gobjects['listbox_url_info_references'],
                metadata['referenceUrls'],
                signals={'activate-link': self.signal_label_activate_link})
        gui_utilities.gtk_listbox_populate_labels(
            self.gobjects['listbox_url_info_classifiers'],
            metadata['classifiers'])

    def __async_rpc_cb_populate_url_scheme_combobox(self, addresses):
        addresses = sorted(addresses, key=lambda address: address['port'])
        combobox_url_scheme = self.gobjects['combobox_url_scheme']
        model = combobox_url_scheme.get_model()
        for address in addresses:
            if address['ssl']:
                scheme_name = 'https'
                description = '' if address['port'] == 443 else 'port: ' + str(
                    address['port'])
            else:
                scheme_name = 'http'
                description = '' if address['port'] == 80 else 'port: ' + str(
                    address['port'])
            # use the scheme and port to make a row UID
            model.append(
                _ModelURLScheme(id=scheme_name + '/' + str(address['port']),
                                name=scheme_name,
                                description=description,
                                port=address['port']))
        if gui_utilities.gtk_list_store_search(model, 'https/443'):
            combobox_url_scheme.set_active_id('https/443')
        elif gui_utilities.gtk_list_store_search(model, 'http/80'):
            combobox_url_scheme.set_active_id('http/80')

    def __async_rpc_cb_changed_url_hostname(self, hostname, results):
        self._cache_hostname[hostname] = results
        templates = results['siteTemplates']
        combobox = self.gobjects['combobox_url_path']
        combobox.set_property('button-sensitivity', templates['total'] > 0)
        model = combobox.get_model()
        model.clear()
        for template in templates['edges']:
            template = template['node']
            path = utilities.make_webrelpath(template['path'])
            if path and not path.endswith(webpath.sep):
                path += webpath.sep
            for page in template['metadata']['pages']:
                model.append((path + utilities.make_webrelpath(page), path))
        # this is going to trigger a changed signal and the cascade effect will update the URL information and preview
        combobox.set_active_id(
            utilities.make_webrelpath(
                gui_utilities.gtk_combobox_get_entry_text(combobox)))

        sni_hostname = results['ssl']['sniHostname']
        label = self.gobjects['label_url_ssl_status']
        if sni_hostname is None:
            _set_icon(self.gobjects['image_url_ssl_status'], 'gtk-no')
            label.set_text(
                'There is no certificate available for the specified hostname')
            if self._can_issue_certs:
                self.gobjects[
                    'button_url_ssl_issue_certificate'].set_sensitive(True)
        else:
            _set_icon(self.gobjects['image_url_ssl_status'], 'gtk-yes')
            label.set_text(
                'A certificate for the specified hostname is available')

    def __async_rpc_cb_ssl_status(self, results):
        self._ssl_status = results['ssl']['status']
        self._can_issue_certs = all(results['ssl']['status'].values())

    @property
    def campaign_name(self):
        """
		The string value of the configured campaign name. This may be set even
		when the campaign was not created, which would be the case if the user
		closed the window.
		"""
        return self.gobjects['entry_campaign_name'].get_text()

    @property
    def is_editing_campaign(self):
        return self.campaign_id is not None

    @property
    def is_new_campaign(self):
        return self.campaign_id is None

    def _do_regex_validation(self, test_text, entry):
        try:
            regex = re.compile(entry.get_text())
        except re.error:
            entry.set_property('secondary-icon-stock', 'gtk-dialog-warning')
            return
        result = True
        if regex.pattern and test_text:
            result = regex.match(test_text) is not None
        entry.set_property('secondary-icon-stock',
                           'gtk-yes' if result else 'gtk-no')

    def _get_campaign_by_name(self, name):
        campaign = self.application.rpc.graphql(
            """\
			query getCampaignByName($name: String!)
			{ db { campaign(name: $name) { id } } }
		""",
            query_vars={'name': name})['db']['campaign']
        return campaign

    def _get_company_existing_id(self):
        combobox = self.gobjects['combobox_company_existing']
        model = combobox.get_model()
        model_iter = combobox.get_active_iter()
        if model is None:
            return
        if model_iter is None:
            text = combobox.get_child().get_text().strip()
            if text:
                model_iter = gui_utilities.gtk_list_store_search(model,
                                                                 text,
                                                                 column=1)
        if model_iter is None:
            return None
        return model.get_value(model_iter, 0)

    def _get_company_new_id(self):
        name = self.gobjects['entry_company_name'].get_text()
        name = name.strip()
        # check if this company name already exists in the model
        model = self.gobjects['combobox_company_existing'].get_model()
        model_iter = gui_utilities.gtk_list_store_search(model, name, column=1)
        if model_iter is not None:
            return model.get_value(model_iter, 0)
        # check if this company name already exists remotely
        remote_company = self._get_graphql_company(name)
        if remote_company:
            return remote_company['id']

        company_id = self.application.rpc(
            'db/table/insert', 'companies',
            ('name', 'description', 'industry_id', 'url_main', 'url_email',
             'url_remote_access'),
            (name, self.get_entry_value('company_description'),
             self._get_tag_from_combobox(
                 self.gobjects['combobox_company_industry'],
                 'industries'), self.get_entry_value('company_url_main'),
             self.get_entry_value('company_url_email'),
             self.get_entry_value('company_url_remote_access')))
        self.gobjects['radiobutton_company_existing'].set_active(True)
        return company_id

    def _get_graphql_company(self, company_name):
        results = self.application.rpc.graphql(
            """\
		query getCompany($name: String!) {
			db {
				company(name: $name) {
					id
				}
			}
		}""", {'name': company_name})
        return results['db']['company']

    def _get_kpm_path(self):
        file_path = self.gobjects['entry_kpm_file'].get_text()
        dir_path = self.gobjects['entry_kpm_dest_folder'].get_text()
        if not dir_path and not file_path:
            return _KPMPaths(None, None, True)
        if not _kpm_file_path_is_valid(file_path):
            return _KPMPaths(None, None, False)
        if not (dir_path and os.path.isdir(dir_path)
                and os.access(dir_path, os.R_OK | os.W_OK)):
            return _KPMPaths(None, None, False)
        return _KPMPaths(file_path, dir_path, True)

    def _get_tag_from_combobox(self, combobox, db_table):
        model = combobox.get_model()
        model_iter = combobox.get_active_iter()
        if model_iter is not None:
            return model.get_value(model_iter, 0)
        text = combobox.get_child().get_text().strip()
        if not text:
            return
        model_iter = gui_utilities.gtk_list_store_search(model, text, column=1)
        if model_iter is None:
            return self.application.rpc('db/table/insert', db_table, 'name',
                                        text)
        return model.get_value(model_iter, 0)

    def _get_webserver_url(self):
        return self.gobjects['label_url_preview'].get_text()

    @property
    def _server_uses_ssl(self):
        return any(
            address['ssl']
            for address in self.config['server_config']['server.addresses'])

    def _set_comboboxes(self):
        """Set up all the comboboxes and load the data for their models."""
        renderer = resources.renderer_text_desc
        rpc = self.application.rpc
        for tag_name, tag_table in (('campaign_type', 'campaign_types'),
                                    ('company_existing', 'companies'),
                                    ('company_industry', 'industries')):
            combobox = self.gobjects['combobox_' + tag_name]
            model = combobox.get_model()
            if model is None:
                combobox.pack_start(renderer, True)
                combobox.add_attribute(renderer, 'text', 2)
            combobox.set_model(rpc.get_tag_model(tag_table, model=model))
            gui_utilities.gtk_combobox_set_entry_completion(combobox)
        # setup the URL scheme combobox asynchronously
        model = Gtk.ListStore(str, str, str, int)
        combobox = self.gobjects['combobox_url_scheme']
        combobox.set_model(model)
        combobox.pack_start(renderer, True)
        combobox.add_attribute(renderer, 'text', 2)
        rpc.async_call(
            'config/get', ('server.addresses', ),
            on_success=self.__async_rpc_cb_populate_url_scheme_combobox,
            when_idle=True)
        # setup the URL hostname combobox asynchronously
        model = Gtk.ListStore(str)
        combobox = self.gobjects['combobox_url_hostname']
        combobox.set_model(model)
        gui_utilities.gtk_combobox_set_entry_completion(combobox)
        rpc.async_call(
            'hostnames/get',
            on_success=self.__async_rpc_cb_populate_url_hostname_combobox,
            when_idle=True)
        # setup the URL path combobox model, but don't populate it until a hostname is selected
        model = Gtk.ListStore(str, str)
        combobox = self.gobjects['combobox_url_path']
        combobox.set_model(model)
        gui_utilities.gtk_combobox_set_entry_completion(combobox)

    def _set_defaults(self):
        """
		Set any default values for widgets. Also load settings from the existing
		campaign if one was specified.
		"""
        calendar = self.gobjects['calendar_campaign_expiration']
        default_day = datetime.datetime.today() + datetime.timedelta(days=31)
        gui_utilities.gtk_calendar_set_pydate(calendar, default_day)

        if self.is_new_campaign:
            return
        campaign = self.application.get_graphql_campaign()

        # set entries
        self.gobjects['entry_campaign_name'].set_text(campaign['name'])
        self.gobjects['entry_validation_regex_username'].set_text(
            campaign['credentialRegexUsername'] or '')
        self.gobjects['entry_validation_regex_password'].set_text(
            campaign['credentialRegexPassword'] or '')
        self.gobjects['entry_validation_regex_mfa_token'].set_text(
            campaign['credentialRegexMfaToken'] or '')

        if campaign['description'] is not None:
            self.gobjects['entry_campaign_description'].set_text(
                campaign['description'])
        if campaign['campaignType'] is not None:
            combobox = self.gobjects['combobox_campaign_type']
            model = combobox.get_model()
            model_iter = gui_utilities.gtk_list_store_search(
                model, campaign['campaignType']['id'], column=0)
            if model_iter is not None:
                combobox.set_active_iter(model_iter)

        self.gobjects['checkbutton_alert_subscribe'].set_property(
            'active',
            self.application.rpc('campaign/alerts/is_subscribed',
                                 self.campaign_id))
        self.gobjects['checkbutton_reject_after_credentials'].set_property(
            'active', bool(campaign['maxCredentials']))

        if campaign['company'] is not None:
            self.gobjects['radiobutton_company_existing'].set_active(True)
            combobox = self.gobjects['combobox_company_existing']
            model = combobox.get_model()
            model_iter = gui_utilities.gtk_list_store_search(
                model, campaign['company']['id'], column=0)
            if model_iter is not None:
                combobox.set_active_iter(model_iter)

        if campaign['expiration'] is not None:
            expiration = utilities.datetime_utc_to_local(
                campaign['expiration'])
            self.gobjects['checkbutton_expire_campaign'].set_active(True)
            self._expiration_time.time = expiration.time()
            gui_utilities.gtk_calendar_set_pydate(
                self.gobjects['calendar_campaign_expiration'],
                expiration.date())

    def _set_webserver_url(self, webserver_url):
        webserver_url = urllib.parse.urlparse(webserver_url.strip())
        if webserver_url.scheme == 'http':
            self.gobjects['combobox_url_scheme'].set_active_id(
                'http/' + str(webserver_url.port or 80))
        elif webserver_url.scheme == 'https':
            self.gobjects['combobox_url_scheme'].set_active_id(
                'https/' + str(webserver_url.port or 443))
        if webserver_url.hostname:
            self.gobjects['combobox_url_hostname'].get_child().set_text(
                webserver_url.hostname)
        if webserver_url.path:
            self.gobjects['combobox_url_path'].get_child().set_text(
                utilities.make_webrelpath(webserver_url.path))

    def _set_page_complete(self, complete, page=None):
        if page is None:
            page = self.assistant.get_nth_page(
                self.assistant.get_current_page())
        elif isinstance(page, str):
            page = self.assistant.get_nth_page(self._page_titles[page])
        elif isinstance(page, int):
            page = self.assistant.get_nth_page(page)
        self.assistant.set_page_complete(page, complete)

    def signal_assistant_apply(self, _):
        self._close_ready = False
        # have to do it this way because the next page will be selected when the apply signal is complete
        set_current_page = lambda page_name: self.assistant.set_current_page(
            max(0, self._page_titles[page_name] - 1))

        # get and validate the campaign name
        campaign_name = self.gobjects['entry_campaign_name'].get_text()
        campaign_name = campaign_name.strip()
        if not campaign_name:
            gui_utilities.show_dialog_error(
                'Invalid Campaign Name', self.parent,
                'A valid campaign name must be specified.')
            set_current_page('Basic Settings')
            return True
        campaign = self._get_campaign_by_name(campaign_name)
        if campaign and campaign['id'] != self.campaign_id:
            gui_utilities.show_dialog_error(
                'Invalid Campaign Name', self.parent,
                'A unique campaign name must be specified.')
            set_current_page('Basic Settings')
            return True

        properties = {}
        # validate the credential validation regular expressions
        for field in ('username', 'password', 'mfa_token'):
            regex = self.gobjects['entry_validation_regex_' + field].get_text()
            if regex:
                try:
                    re.compile(regex)
                except re.error:
                    label = self.gobjects['label_validation_regex_' +
                                          field].get_text()
                    gui_utilities.show_dialog_error(
                        'Invalid Regex', self.parent,
                        "The '{0}' regular expression is invalid.".format(
                            label))
                    return True
            else:
                regex = None  # keep empty strings out of the database
            properties['credential_regex_' + field] = regex

        # validate the company
        company_id = None
        if self.gobjects['radiobutton_company_existing'].get_active():
            company_id = self._get_company_existing_id()
            if company_id is None:
                gui_utilities.show_dialog_error(
                    'Invalid Company', self.parent,
                    'A valid existing company must be specified.')
                set_current_page('Company')
                return True
        elif self.gobjects['radiobutton_company_new'].get_active():
            company_id = self._get_company_new_id()
            if company_id is None:
                gui_utilities.show_dialog_error(
                    'Invalid Company', self.parent,
                    'The new company settings are invalid.')
                set_current_page('Company')
                return True

        # get and validate the campaign expiration
        expiration = None
        if self.gobjects['checkbutton_expire_campaign'].get_property('active'):
            expiration = datetime.datetime.combine(
                gui_utilities.gtk_calendar_get_pydate(
                    self.gobjects['calendar_campaign_expiration']),
                self._expiration_time.time)
            expiration = utilities.datetime_local_to_utc(expiration)
            if self.is_new_campaign and expiration <= datetime.datetime.now():
                gui_utilities.show_dialog_error(
                    'Invalid Campaign Expiration', self.parent,
                    'The expiration date is set in the past.')
                set_current_page('Expiration')
                return True

        # point of no return
        campaign_description = self.get_entry_value('campaign_description')
        if self.campaign_id:
            properties['name'] = self.campaign_name
            properties['description'] = campaign_description
            cid = self.campaign_id
        else:
            try:
                cid = self.application.rpc('campaign/new',
                                           campaign_name,
                                           description=campaign_description)
                properties['name'] = campaign_name
            except advancedhttpserver.RPCError as error:
                if not error.is_remote_exception:
                    raise error
                if not error.remote_exception[
                        'name'] == 'exceptions.ValueError':
                    raise error
                error_message = error.remote_exception.get(
                    'message', 'an unknown error occurred').capitalize() + '.'
                gui_utilities.show_dialog_error('Failed To Create Campaign',
                                                self.parent, error_message)
                set_current_page('Basic Settings')
                return True
            self.application.emit('campaign-created', cid)

        properties['campaign_type_id'] = self._get_tag_from_combobox(
            self.gobjects['combobox_campaign_type'], 'campaign_types')
        properties['company_id'] = company_id
        properties['expiration'] = expiration
        properties['max_credentials'] = (
            1 if self.gobjects['checkbutton_reject_after_credentials'].
            get_property('active') else None)
        self.application.rpc('db/table/set', 'campaigns', cid,
                             tuple(properties.keys()),
                             tuple(properties.values()))

        should_subscribe = self.gobjects[
            'checkbutton_alert_subscribe'].get_property('active')
        if should_subscribe != self.application.rpc(
                'campaign/alerts/is_subscribed', cid):
            if should_subscribe:
                self.application.rpc('campaign/alerts/subscribe', cid)
            else:
                self.application.rpc('campaign/alerts/unsubscribe', cid)

        self.application.emit('campaign-changed', cid)

        old_cid = self.config.get('campaign_id')
        self.config['campaign_id'] = cid
        self.config['campaign_name'] = properties['name']

        _kpm_pathing = self._get_kpm_path()
        if all(_kpm_pathing):
            if not self.application.main_tabs['mailer'].emit(
                    'message-data-import', _kpm_pathing.kpm_file,
                    _kpm_pathing.destination_folder):
                gui_utilities.show_dialog_info(
                    'Failure', self.parent,
                    'Failed to import the message configuration.')
            else:
                gui_utilities.show_dialog_info(
                    'Success', self.parent,
                    'Successfully imported the message configuration.')

        if self._ssl_status['hasSni']:
            combobox_url_scheme = self.gobjects['combobox_url_scheme']
            active = combobox_url_scheme.get_active()
            if active != -1:
                url_scheme = _ModelURLScheme(
                    *combobox_url_scheme.get_model()[active])
                if url_scheme.name == 'https':
                    hostname = gui_utilities.gtk_combobox_get_entry_text(
                        self.gobjects['combobox_url_hostname'])
                    if not self.application.rpc('ssl/sni_hostnames/load',
                                                hostname):
                        gui_utilities.show_dialog_error(
                            'Failure', self.parent,
                            'Failed to load an SSL certificate for the specified hostname.'
                        )

        self.config['mailer.webserver_url'] = self._get_webserver_url()
        self.application.emit('campaign-set', old_cid, cid)
        self._close_ready = True
        return

    def signal_assistant_cancel(self, assistant):
        assistant.destroy()

    def signal_assistant_close(self, assistant):
        if self._close_ready:
            assistant.destroy()
        self._close_ready = True

    def signal_assistant_prepare(self, _, page):
        page_title = self.assistant.get_page_title(page)
        if page_title == 'Company':
            combobox = self.gobjects['combobox_company_existing']
            model = combobox.get_model()
            company_name = self.get_entry_value('company_name')
            if company_name:
                model_iter = gui_utilities.gtk_list_store_search(model,
                                                                 company_name,
                                                                 column=1)
                if model_iter is not None:
                    combobox.set_active_iter(model_iter)
                    self.gobjects['radiobutton_company_existing'].set_active(
                        True)

    def signal_button_clicked_issue_certificate(self, button):
        button.set_sensitive(False)
        self._set_page_complete(False, page='Web Server URL')
        label = self.gobjects['label_url_ssl_status']
        label.set_text(
            'A certificate for the specified hostname is being requested')
        hostname = gui_utilities.gtk_combobox_get_entry_text(
            self.gobjects['combobox_url_hostname'])
        self.application.rpc.async_call(
            'ssl/letsencrypt/issue', (hostname, ),
            on_error=self.__async_rpc_cb_issue_cert_error,
            on_success=self.__async_rpc_cb_issue_cert_success,
            when_idle=True)

    def signal_calendar_prev(self, calendar):
        today = datetime.date.today()
        calendar_day = gui_utilities.gtk_calendar_get_pydate(calendar)
        if calendar_day >= today:
            return
        gui_utilities.gtk_calendar_set_pydate(calendar, today)

    def signal_checkbutton_expire_campaign_toggled(self, _):
        active = self.gobjects['checkbutton_expire_campaign'].get_property(
            'active')
        self.gobjects['frame_campaign_expiration'].set_sensitive(active)

    @gui_utilities.delayed_signal()
    def signal_combobox_changed_set_url_information(self, _):
        for label in ('info_title', 'info_authors', 'info_created',
                      'info_description'):
            self.gobjects['label_url_' + label].set_text('')
        hostname = gui_utilities.gtk_combobox_get_entry_text(
            self.gobjects['combobox_url_hostname'])
        if not hostname:
            return
        combobox_url_path = self.gobjects['combobox_url_path']
        path = gui_utilities.gtk_combobox_get_active_cell(combobox_url_path,
                                                          column=1)
        if path is None:
            model = combobox_url_path.get_model()
            text = utilities.make_webrelpath(
                gui_utilities.gtk_combobox_get_entry_text(combobox_url_path))
            row_iter = gui_utilities.gtk_list_store_search(model, text)
            if row_iter:
                path = model[row_iter][1]
        gui_utilities.gtk_widget_destroy_children(
            self.gobjects['listbox_url_info_classifiers'])
        gui_utilities.gtk_widget_destroy_children(
            self.gobjects['listbox_url_info_references'])
        cached_result = self._cache_site_template.get((hostname, path))
        if cached_result:
            self.__async_rpc_cb_populate_url_info(hostname, path,
                                                  cached_result)
            return
        self.application.rpc.async_graphql(
            """
			query getSiteTemplate($hostname: String, $path: String) {
			  siteTemplate(hostname: $hostname, path: $path) {
				created path metadata { title authors description referenceUrls classifiers pages }
			  }
			}
			""",
            query_vars={
                'hostname': hostname,
                'path': path
            },
            on_success=self.__async_rpc_cb_populate_url_info,
            cb_args=(hostname, path),
            when_idle=True)

    def signal_combobox_changed_set_url_preview(self, _):
        label = self.gobjects['label_url_preview']
        label.set_text('')
        combobox_url_scheme = self.gobjects['combobox_url_scheme']
        active = combobox_url_scheme.get_active()
        if active == -1:
            return
        url_scheme = _ModelURLScheme(*combobox_url_scheme.get_model()[active])
        authority = gui_utilities.gtk_combobox_get_entry_text(
            self.gobjects['combobox_url_hostname'])
        path = gui_utilities.gtk_combobox_get_entry_text(
            self.gobjects['combobox_url_path'])
        if url_scheme and authority:
            path = utilities.make_webrelpath(path)
            if (url_scheme.name == 'http'
                    and url_scheme.port != 80) or (url_scheme.name == 'https'
                                                   and url_scheme.port != 443):
                authority += ':' + str(url_scheme.port)
            label.set_text("{}://{}/{}".format(url_scheme.name, authority,
                                               path))

    @gui_utilities.delayed_signal()
    def signal_combobox_changed_url_hostname(self, combobox):
        self.gobjects['button_url_ssl_issue_certificate'].set_sensitive(False)
        hostname = gui_utilities.gtk_combobox_get_entry_text(combobox)
        if not hostname:
            return
        cached_result = self._cache_hostname.get(hostname)
        if cached_result:
            self.__async_rpc_cb_changed_url_hostname(hostname, cached_result)
            return
        self.application.rpc.async_graphql(
            """
			query getHostname($hostname: String) {
			  siteTemplates(hostname: $hostname) {
				total edges { node { hostname path metadata { pages } } }
			  }
			  ssl { sniHostname(hostname: $hostname) { enabled } }
			}
			""",
            query_vars={'hostname': hostname},
            on_success=self.__async_rpc_cb_changed_url_hostname,
            cb_args=(hostname, ),
            when_idle=True)

    def signal_combobox_changed_url_scheme(self, combobox):
        active = combobox.get_active()
        if active == -1:
            return
        url_scheme = _ModelURLScheme(*combobox.get_model()[active])
        revealer = self.gobjects['revealer_url_ssl_settings']
        revealer.set_reveal_child(url_scheme.name == 'https')

    def signal_entry_changed_campaign_name(self, entry):
        campaign_name = entry.get_text().strip()
        if not campaign_name:
            entry.set_property('secondary-icon-stock', 'gtk-dialog-warning')
        if self.is_new_campaign:
            if self._campaign_names.get(campaign_name) is not None:
                entry.set_property('secondary-icon-stock',
                                   'gtk-dialog-warning')
            else:
                entry.set_property('secondary-icon-stock', None)
        elif self.is_editing_campaign:
            other_cid = self._campaign_names.get(campaign_name)
            if other_cid is not None and other_cid != self.campaign_id:
                entry.set_property('secondary-icon-stock',
                                   'gtk-dialog-warning')
            else:
                entry.set_property('secondary-icon-stock', None)

    def signal_entry_changed_test_validation_text(self, field):
        test_text = field.get_text()
        for field in ('username', 'password', 'mfa_token'):
            self._do_regex_validation(
                test_text, self.gobjects['entry_validation_regex_' + field])

    def signal_entry_changed_validation_regex(self, entry):
        self._do_regex_validation(
            self.gobjects['entry_test_validation_text'].get_text(), entry)

    def signal_label_activate_link(self, _, uri):
        utilities.open_uri(uri)

    def signal_kpm_select_clicked(self, _):
        dialog = extras.FileChooserDialog('Import Message Configuration',
                                          self.parent)
        dialog.quick_add_filter('King Phisher Message Files', '*.kpm')
        dialog.quick_add_filter('All Files', '*')
        response = dialog.run_quick_open()
        dialog.destroy()
        if not response:
            return False
        target_path = response['target_path']
        self.gobjects['entry_kpm_file'].set_text(target_path)
        self._set_page_complete(self._get_kpm_path().is_valid)

        if not _kpm_file_path_is_valid(target_path):
            return
        # open the KPM for reading to extract the target URL for the assistant,
        # ignore the directory to allow the user to optionally only import the URL
        kpm = archive.ArchiveFile(target_path, 'r')
        if not kpm.has_file('message_config.json'):
            self.logger.warning(
                'the kpm archive is missing the message_config.json file')
            return
        message_config = kpm.get_json('message_config.json')
        webserver_url = message_config.get('webserver_url')
        if not webserver_url:
            return
        self._set_webserver_url(webserver_url)

    def signal_kpm_dest_folder_clicked(self, _):
        dialog = extras.FileChooserDialog('Destination Directory', self.parent)
        response = dialog.run_quick_select_directory()
        dialog.destroy()
        if not response:
            return False
        self.gobjects['entry_kpm_dest_folder'].set_text(
            response['target_path'])
        self._set_page_complete(self._get_kpm_path().is_valid)

    def signal_kpm_entry_clear(self, entry_widget):
        entry_widget.set_text('')
        self._set_page_complete(self._get_kpm_path().is_valid)

    def signal_radiobutton_toggled(self, radiobutton):
        if not radiobutton.get_active():
            return
        if radiobutton == self.gobjects['radiobutton_company_existing']:
            self.gobjects['frame_company_existing'].set_sensitive(True)
            self.gobjects['frame_company_new'].set_sensitive(False)
        elif radiobutton == self.gobjects['radiobutton_company_new']:
            self.gobjects['frame_company_existing'].set_sensitive(False)
            self.gobjects['frame_company_new'].set_sensitive(True)
        elif radiobutton == self.gobjects['radiobutton_company_none']:
            self.gobjects['frame_company_existing'].set_sensitive(False)
            self.gobjects['frame_company_new'].set_sensitive(False)

    def interact(self):
        self.assistant.show_all()
Beispiel #20
0
class MailSenderConfigurationTab(gui_utilities.GladeGObject):
	"""
	This is the tab which allows the user to configure and set parameters
	for sending messages as part of a campaign.
	"""
	dependencies = gui_utilities.GladeDependencies(
		children=(
			'button_target_file_select',
			'calendar_calendar_invite_date',
			'checkbutton_calendar_invite_all_day',
			'checkbutton_calendar_request_rsvp',
			'combobox_importance',
			'combobox_sensitivity',
			'entry_webserver_url',
			'entry_calendar_invite_location',
			'entry_calendar_invite_summary',
			'entry_company_name',
			'entry_source_email',
			'entry_source_email_smtp',
			'entry_source_email_alias',
			'entry_subject',
			'entry_reply_to_email',
			'entry_html_file',
			'entry_target_file',
			'entry_target_name',
			'entry_target_email_address',
			'entry_attachment_file',
			'expander_calendar_invite_settings',
			'expander_email_settings',
			'radiobutton_message_type_calendar_invite',
			'radiobutton_message_type_email',
			'radiobutton_target_type_file',
			'radiobutton_target_type_single',
			'spinbutton_calendar_invite_duration',
			'spinbutton_calendar_invite_start_hour',
			'spinbutton_calendar_invite_start_minute',
			'viewport'
		),
		top_level=(
			'ClockHourAdjustment',
			'ClockMinuteAdjustment',
			'TimeDuration',
			'MsgImportance',
			'MsgSensitivity'
		)
	)
	config_prefix = 'mailer.'
	top_gobject = 'box'
	def __init__(self, *args, **kwargs):
		self.label = Gtk.Label(label='Configuration')
		"""The :py:class:`Gtk.Label` representing this tabs name."""
		super(MailSenderConfigurationTab, self).__init__(*args, **kwargs)
		self.application.connect('campaign-changed', self.signal_kpc_campaign_load)
		self.application.connect('campaign-set', self.signal_kpc_campaign_load)
		self.application.connect('exit', self.signal_kpc_exit)

		self.message_type = managers.RadioButtonGroupManager(self, 'message_type')
		self.message_type.set_active(self.config['mailer.message_type'])
		self.target_type = managers.RadioButtonGroupManager(self, 'target_type')
		self.target_type.set_active(self.config['mailer.target_type'])

	def objects_load_from_config(self):
		super(MailSenderConfigurationTab, self).objects_load_from_config()
		# these are called in the super class's __init__ method so they may not exist yet
		if hasattr(self, 'message_type'):
			self.message_type.set_active(self.config['mailer.message_type'])
		if hasattr(self, 'target_type'):
			self.target_type.set_active(self.config['mailer.target_type'])

	def objects_save_to_config(self):
		super(MailSenderConfigurationTab, self).objects_save_to_config()
		self.config['mailer.message_type'] = self.message_type.get_active()
		self.config['mailer.target_type'] = self.target_type.get_active()

	def signal_button_clicked_verify(self, button):
		target_url = self.gobjects['entry_webserver_url'].get_text()
		error_description = None
		if re.match(r'^\s+', target_url):
			target_url = target_url.strip()
			self.gobjects['entry_webserver_url'].set_text(target_url)
		for _ in range(1):
			if not target_url.strip().startswith('http'):
				error_description = 'The web server URL is invalid'
				break

			try:
				response = test_webserver_url(target_url, self.config['server_config']['server.secret_id'])
			except (requests.exceptions.ConnectionError, requests.exceptions.RequestException) as error:
				if isinstance(error, requests.exceptions.ConnectionError):
					self.logger.warning('verify url attempt failed, could not connect')
					error_description = 'Could not connect to the server'
				elif isinstance(error, requests.exceptions.Timeout):
					self.logger.warning('verify url attempt failed, a timeout occurred')
					error_description = 'The HTTP request timed out'
				else:
					self.logger.warning('unknown verify url exception: ' + repr(error))
					error_description = 'An unknown verify URL exception occurred'
				break

			if not response.ok:
				self.logger.warning("verify url HTTP error: {0} {1}".format(response.status_code, response.reason))
				error_description = "HTTP status {0} {1}".format(response.status_code, response.reason)
				break

			self.logger.debug("verify url HTTP status: {0} {1}".format(response.status_code, response.reason))
		if error_description:
			gui_utilities.show_dialog_warning('Unable To Open The Web Server URL', self.parent, error_description)
		else:
			gui_utilities.show_dialog_info('Successfully Opened The Web Server URL', self.parent)
		return

	def signal_checkbutton_toggled_calendar_invite_all_day(self, button):
		all_day = button.get_active()
		self.gobjects['spinbutton_calendar_invite_duration'].set_sensitive(not all_day)
		self.gobjects['spinbutton_calendar_invite_start_hour'].set_sensitive(not all_day)
		self.gobjects['spinbutton_calendar_invite_start_minute'].set_sensitive(not all_day)

	def signal_entry_activate_open_file(self, entry):
		dialog = extras.FileChooserDialog('Choose File', self.parent)
		if entry == self.gobjects.get('entry_html_file'):
			dialog.quick_add_filter('HTML Files', ['*.htm', '*.html'])
		elif entry == self.gobjects.get('entry_target_file'):
			dialog.quick_add_filter('CSV Files', '*.csv')
		dialog.quick_add_filter('All Files', '*')
		response = dialog.run_quick_open()
		dialog.destroy()
		if not response:
			return False
		entry.set_text(response['target_path'])
		return True

	def signal_entry_backspace(self, entry):
		entry.set_text('')
		return True

	def signal_expander_activate_message_type(self, expander):
		if expander.get_expanded():
			# ignore attempts to un-expand
			expander.set_expanded(False)
			return
		if expander == self.gobjects['expander_calendar_invite_settings']:
			message_type = 'calendar_invite'
			self.gobjects['expander_email_settings'].set_expanded(False)
		elif expander == self.gobjects['expander_email_settings']:
			message_type = 'email'
			self.gobjects['expander_calendar_invite_settings'].set_expanded(False)
		button = self.message_type.buttons[message_type]
		with gui_utilities.gobject_signal_blocked(button, 'toggled'):
			self.message_type.set_active(message_type)

	def signal_expander_notify_expanded(self, expander, _):
		if expander.get_expanded():
			self.gobjects['viewport'].queue_draw()

	def signal_kpc_campaign_load(self, _, campaign_id):
		campaign = self.application.rpc.remote_table_row('campaigns', campaign_id, cache=True, refresh=True)
		if campaign.company_id is None:
			self.config['mailer.company_name'] = None
		else:
			self.config['mailer.company_name'] = campaign.company.name
		self.gobjects['entry_company_name'].set_text(self.config['mailer.company_name'] or '')

	def signal_kpc_exit(self, kpc):
		self.objects_save_to_config()

	def signal_radiobutton_toggled_message_type(self, radiobutton):
		if not radiobutton.get_active():
			return
		message_type = self.message_type.get_active()
		self.gobjects['expander_calendar_invite_settings'].set_expanded(message_type == 'calendar_invite')
		self.gobjects['expander_email_settings'].set_expanded(message_type == 'email')

	def signal_radiobutton_toggled_target_type(self, radiobutton):
		if not radiobutton.get_active():
			return
		target_type = self.target_type.get_active()
		self.gobjects['button_target_file_select'].set_sensitive(target_type == 'file')
		self.gobjects['entry_target_file'].set_sensitive(target_type == 'file')
		self.gobjects['entry_target_name'].set_sensitive(target_type == 'single')
		self.gobjects['entry_target_email_address'].set_sensitive(target_type == 'single')
Beispiel #21
0
class MailSenderEditTab(gui_utilities.GladeGObject):
	"""
	This is the tab which adds basic text edition for changing an email
	template.
	"""
	dependencies = gui_utilities.GladeDependencies(
		children=(
			'toolbutton_save_as_html_file',
			'toolbutton_save_html_file',
			'view_html_file'
		)
	)
	top_gobject = 'box'
	def __init__(self, *args, **kwargs):
		self.label = Gtk.Label(label='Edit')
		"""The :py:class:`Gtk.Label` representing this tabs name."""
		super(MailSenderEditTab, self).__init__(*args, **kwargs)
		self.textview = self.gobjects['view_html_file']
		"""The :py:class:`Gtk.TextView` object of the editor."""
		self.textbuffer = GtkSource.Buffer()
		"""The :py:class:`Gtk.TextBuffer` used by the :py:attr:textview` attribute."""
		self.textview.set_buffer(self.textbuffer)
		self.textview.modify_font(Pango.FontDescription(self.config['text_font']))
		self.language_manager = GtkSource.LanguageManager()
		self.textbuffer.set_language(self.language_manager.get_language('html'))
		self.textbuffer.set_highlight_syntax(True)
		self.toolbutton_save_html_file = self.gobjects['toolbutton_save_html_file']
		self.textview.connect('populate-popup', self.signal_textview_populate_popup)
		self.textview.connect('key-press-event', self.signal_textview_key_pressed)

		scheme_manager = GtkSource.StyleSchemeManager()
		style_scheme_name = self.config['text_source_theme']
		style_scheme = scheme_manager.get_scheme(style_scheme_name)
		if style_scheme:
			self.textbuffer.set_style_scheme(style_scheme)
		else:
			self.logger.error("invalid GTK source theme: '{0}'".format(style_scheme_name))
		self.file_monitor = None

		source_completion = self.textview.get_completion()
		source_completion.set_property('accelerators', 0)
		source_completion.set_property('auto-complete-delay', 250)
		source_completion.set_property('show-icons', False)
		source_completion.add_provider(completion_providers.HTMLComletionProvider())
		source_completion.add_provider(completion_providers.JinjaEmailCompletionProvider())

	def _html_file_changed(self, path, monitor_event):
		if monitor_event in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CHANGES_DONE_HINT, Gio.FileMonitorEvent.CREATED):
			self.load_html_file()

	def load_html_file(self):
		"""Load the contents of the configured HTML file into the editor."""
		html_file = self.config.get('mailer.html_file')
		if not (html_file and os.path.isfile(html_file) and os.access(html_file, os.R_OK)):
			self.toolbutton_save_html_file.set_sensitive(False)
			return
		self.toolbutton_save_html_file.set_sensitive(True)
		with codecs.open(html_file, 'r', encoding='utf-8') as file_h:
			html_data = file_h.read()
		self.textbuffer.begin_not_undoable_action()
		self.textbuffer.set_text(html_data)
		self.textbuffer.end_not_undoable_action()

	def save_html_file(self, force_prompt=False):
		"""
		Save the contents from the editor into an HTML file if one is configured
		otherwise prompt to user to select a file to save as. The user may abort
		the operation by declining to select a file to save as if they are
		prompted to do so.

		:param force_prompt: Force prompting the user to select the file to save as.
		:rtype: bool
		:return: Whether the contents were saved or not.
		"""
		html_file = self.config.get('mailer.html_file')
		if not html_file or force_prompt:
			if html_file:
				current_name = os.path.basename(html_file)
			else:
				current_name = 'message.html'
			dialog = extras.FileChooserDialog('Save HTML File', self.parent)
			response = dialog.run_quick_save(current_name=current_name)
			dialog.destroy()
			if not response:
				return False
			html_file = response['target_path']
			self.config['mailer.html_file'] = html_file
		text = self.textbuffer.get_text(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), False)
		with open(html_file, 'w') as file_h:
			file_h.write(text)
		self.toolbutton_save_html_file.set_sensitive(True)
		return True

	def signal_toolbutton_open(self, button):
		dialog = extras.FileChooserDialog('Choose File', self.parent)
		dialog.quick_add_filter('HTML Files', ['*.htm', '*.html'])
		dialog.quick_add_filter('All Files', '*')
		response = dialog.run_quick_open()
		dialog.destroy()
		if not response:
			return False
		self.config['mailer.html_file'] = response['target_path']
		self.show_tab()
		return True

	def signal_toolbutton_save(self, toolbutton):
		html_file = self.config.get('mailer.html_file')
		if not html_file:
			return
		if not gui_utilities.show_dialog_yes_no('Save HTML File', self.parent, 'Do you want to save the changes?'):
			return
		self.save_html_file()

	def signal_toolbutton_save_as(self, toolbutton):
		self.save_html_file(force_prompt=True)

	def signal_toolbutton_template_wiki(self, toolbutton):
		utilities.open_uri('https://github.com/securestate/king-phisher/wiki/Templates#message-templates')

	def signal_textview_populate_popup(self, textview, menu):
		# create and populate the 'Insert' submenu
		insert_submenu = Gtk.Menu.new()
		menu_item = Gtk.MenuItem.new_with_label('Insert')
		menu_item.set_submenu(insert_submenu)
		menu.append(menu_item)
		menu_item.show()

		menu_item = Gtk.MenuItem.new_with_label('Inline Image')
		menu_item.connect('activate', self.signal_activate_popup_menu_insert_image)
		insert_submenu.append(menu_item)

		menu_item = Gtk.MenuItem.new_with_label('Tracking Image Tag')
		menu_item.connect('activate', self.signal_activate_popup_menu_insert, '{{ tracking_dot_image_tag }}')
		insert_submenu.append(menu_item)

		menu_item = Gtk.MenuItem.new_with_label('Webserver URL')
		menu_item.connect('activate', self.signal_activate_popup_menu_insert, '{{ url.webserver }}')
		insert_submenu.append(menu_item)

		# create and populate the 'Date & Time' submenu
		insert_datetime_submenu = Gtk.Menu.new()
		menu_item = Gtk.MenuItem.new_with_label('Date & Time')
		menu_item.set_submenu(insert_datetime_submenu)
		insert_submenu.append(menu_item)
		menu_item.show()

		formats = [
			'%a %B %d, %Y',
			'%b %d, %y',
			'%m/%d/%y',
			None,
			'%I:%M %p',
			'%H:%M:%S'
		]
		dt_now = datetime.datetime.now()
		for fmt in formats:
			if fmt:
				menu_item = Gtk.MenuItem.new_with_label(dt_now.strftime(fmt))
				menu_item.connect('activate', self.signal_activate_popup_menu_insert, "{{{{ time.local | strftime('{0}') }}}}".format(fmt))
			else:
				menu_item = Gtk.SeparatorMenuItem()
			insert_datetime_submenu.append(menu_item)

		insert_submenu.show_all()
		return True

	def signal_textview_key_pressed(self, textview, event):
		if event.type != Gdk.EventType.KEY_PRESS:
			return
		keyval = event.get_keyval()[1]
		if event.get_state() != Gdk.ModifierType.CONTROL_MASK:
			return
		if keyval != Gdk.KEY_s:
			return
		self.save_html_file()

	def signal_activate_popup_menu_insert(self, widget, text):
		self.textbuffer.insert_at_cursor(text)
		return True

	def signal_activate_popup_menu_insert_image(self, widget):
		dialog = extras.FileChooserDialog('Choose Image', self.parent)
		dialog.quick_add_filter('Images', ['*.gif', '*.jpeg', '*.jpg', '*.png'])
		dialog.quick_add_filter('All Files', '*')
		response = dialog.run_quick_open()
		dialog.destroy()
		if not response:
			return
		target_path = response['target_path']
		target_path = escape_single_quote(target_path)
		text = "{{{{ inline_image('{0}') }}}}".format(target_path)
		return self.signal_activate_popup_menu_insert(widget, text)

	def show_tab(self):
		"""Load the message HTML file from disk and configure the tab for editing."""
		if self.file_monitor and self.file_monitor.path == self.config['mailer.html_file']:
			return
		if not self.config['mailer.html_file']:
			if self.file_monitor:
				self.file_monitor.stop()
				self.file_monitor = None
			self.toolbutton_save_html_file.set_sensitive(False)
			return
		self.load_html_file()
		self.file_monitor = gui_utilities.FileMonitor(self.config['mailer.html_file'], self._html_file_changed)
class ConfigurationDialog(gui_utilities.GladeGObject):
    """
	Display the King Phisher client configuration dialog. Running this dialog
	via the :py:meth:`.interact` method will cause some server settings to be
	loaded.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=(
            # Server Tab
            'entry_server',
            'entry_server_username',
            'entry_email_address',
            'entry_sms_phone_number',
            'combobox_sms_carrier',
            # SMTP Server Tab
            'entry_smtp_server',
            'entry_smtp_username',
            'frame_smtp_ssh',
            'spinbutton_smtp_max_send_rate',
            'switch_smtp_ssl_enable',
            'switch_smtp_ssh_enable',
            'entry_sftp_client',
            'entry_ssh_server',
            'entry_ssh_username',
            # Client Tab
            'combobox_spf_check_level',
            # Plugins Tab
            'box_plugin_options'),
        top_level=('SMSCarriers', 'SMTPSendRate', 'SPFCheckLevels'))
    top_gobject = 'dialog'

    def __init__(self, *args, **kwargs):
        super(ConfigurationDialog, self).__init__(*args, **kwargs)
        smtp_ssh_enabled = self.gobjects['switch_smtp_ssh_enable'].get_active()
        self.gobjects['entry_smtp_server'].set_sensitive(not smtp_ssh_enabled)
        self.gobjects['frame_smtp_ssh'].set_sensitive(smtp_ssh_enabled)
        # connect to the signal here so the settings can be loaded with modifications
        self.gobjects['switch_smtp_ssh_enable'].connect(
            'notify::active', self.signal_switch_smtp_ssh)
        self._plugin_option_widgets = collections.defaultdict(dict)
        checkbutton = self.gtk_builder_get(
            'checkbutton_pip_install_dependencies')
        checkbutton.set_active(self.config['plugins.pip.install_dependencies'])

    def signal_switch_smtp_ssh(self, switch, _):
        active = switch.get_property('active')
        entry = self.gobjects['entry_smtp_server']
        self.gtk_builder_get('frame_smtp_ssh').set_sensitive(active)
        if active:
            entry.set_sensitive(False)
            current_text = entry.get_text()
            if current_text.startswith('!'):
                entry.set_text(current_text[1:])
            else:
                entry.set_text('localhost:25')
        else:
            entry.set_sensitive(True)

    def signal_toggle_alert_subscribe(self, cbutton):
        active = cbutton.get_property('active')
        if active:
            remote_method = 'campaign/alerts/subscribe'
        else:
            remote_method = 'campaign/alerts/unsubscribe'
        self.application.rpc(remote_method, self.config['campaign_id'])

    def signal_toggle_reject_after_credentials(self, cbutton):
        max_credentials = (1 if cbutton.get_property('active') else None)
        self.application.rpc('db/table/set', 'campaigns',
                             self.config['campaign_id'], 'max_credentials',
                             max_credentials)

    def signal_changed_spf_check_level(self, combobox):
        ti = combobox.get_active_iter()
        if not ti:
            return
        model = combobox.get_model()
        label = self.gtk_builder_get('label_spf_level_description')
        level_description = model[ti][2]
        label.set_text(level_description)

    def _configure_settings_dashboard(self):
        if not graphs.has_matplotlib:
            self.gtk_builder_get('frame_dashboard').set_sensitive(False)
            return
        graph_providers = Gtk.ListStore(str, str)
        for graph in graphs.get_graphs():
            graph = graphs.get_graph(graph)
            graph_providers.append([graph.name_human, graph.name])
        for dash_position in ['top_left', 'top_right', 'bottom']:
            combobox = self.gtk_builder_get('combobox_dashboard_' +
                                            dash_position)
            combobox.set_model(graph_providers)
            ti = gui_utilities.gtk_list_store_search(
                graph_providers,
                self.config.get('dashboard.' + dash_position),
                column=1)
            combobox.set_active_iter(ti)

    def _configure_settings_plugin_options(self, plugin_klass):
        try:
            frame = PluginsConfigurationFrame(self.application, plugin_klass)
        except Exception:
            self.logger.error(
                'can not build the configuration frame for plugin: ' +
                plugin_klass.name,
                exc_info=True)
        else:
            self.gobjects['box_plugin_options'].pack_start(
                frame, True, True, 0)
            self._plugin_option_widgets[
                plugin_klass.name] = frame.option_widgets

    def _configure_settings_plugins(self):
        pm = self.application.plugin_manager
        plugin_klasses = [
            klass for _, klass in pm if klass.options and klass.is_compatible
        ]
        plugin_klasses = sorted(plugin_klasses, key=lambda k: k.title)
        for plugin_klass in plugin_klasses:
            self._configure_settings_plugin_options(plugin_klass)

    def _configure_settings_proxy(self):
        if not self.config['proxy.url']:
            return
        formatted_proxy_url = urllib.parse.urlparse(self.config['proxy.url'])
        netloc = formatted_proxy_url.netloc
        if formatted_proxy_url.username or formatted_proxy_url.password:
            if formatted_proxy_url.port:
                netloc = '{}:{}'.format(formatted_proxy_url.hostname,
                                        formatted_proxy_url.port)
            else:
                netloc = formatted_proxy_url.hostname
            self.gtk_builder_get('entry_proxy_username').set_text(
                formatted_proxy_url.username)
            self.gtk_builder_get('entry_proxy_password').set_text(
                formatted_proxy_url.password)
        proxy_url = urllib.parse.urlunparse(
            (formatted_proxy_url.scheme, netloc, formatted_proxy_url.path, '',
             '', ''))
        self.gtk_builder_get('entry_proxy_url').set_text(proxy_url)

    def _configure_settings_server(self):
        cb_subscribed = self.gtk_builder_get('checkbutton_alert_subscribe')
        cb_reject_after_creds = self.gtk_builder_get(
            'checkbutton_reject_after_credentials')
        entry_beef_hook = self.gtk_builder_get('entry_server_beef_hook')
        server_config = self.application.rpc(
            'config/get',
            ['beef.hook_url', 'server.require_id', 'server.secret_id'])
        entry_beef_hook.set_property('text',
                                     server_config.get('beef.hook_url', ''))
        self.config['server_config']['server.require_id'] = server_config[
            'server.require_id']
        self.config['server_config']['server.secret_id'] = server_config[
            'server.secret_id']
        # older versions of GObject.signal_handler_find seem to have a bug which cause a segmentation fault in python
        if GObject.pygobject_version < (3, 10):
            cb_subscribed.set_property(
                'active',
                self.application.rpc('campaign/alerts/is_subscribed',
                                     self.config['campaign_id']))
            cb_reject_after_creds.set_property(
                'active',
                self.application.get_graphql_campaign()['maxCredentials'])
        else:
            with gui_utilities.gobject_signal_blocked(cb_subscribed,
                                                      'toggled'):
                cb_subscribed.set_property(
                    'active',
                    self.application.rpc('campaign/alerts/is_subscribed',
                                         self.config['campaign_id']))
                cb_reject_after_creds.set_property(
                    'active',
                    self.application.get_graphql_campaign()['maxCredentials'])
        cb_reject_after_creds.set_sensitive(
            self.config['server_config']['server.require_id'])

    def _finialize_settings_dashboard(self):
        dashboard_changed = False
        for dash_position in ['top_left', 'top_right', 'bottom']:
            combobox = self.gtk_builder_get('combobox_dashboard_' +
                                            dash_position)
            ti = combobox.get_active_iter()
            if not ti:
                continue
            graph_providers = combobox.get_model()
            graph_name = graph_providers[ti][1]
            if self.config.get('dashboard.' + dash_position) == graph_name:
                continue
            self.config['dashboard.' + dash_position] = graph_name
            dashboard_changed = True
        if dashboard_changed:
            gui_utilities.show_dialog_info(
                'The dashboard layout has been updated.', self.parent,
                'The new settings will be applied the next time the application starts.'
            )

    def interact(self):
        self._configure_settings_dashboard()
        self._configure_settings_plugins()
        self._configure_settings_server()
        self._configure_settings_proxy()
        self.gtk_builder_get('combobox_spf_check_level').emit('changed')

        self.dialog.show_all()
        response = self.dialog.run()
        if response != Gtk.ResponseType.CANCEL:
            self.objects_save_to_config()
            self.save_proxy_settings()
            self.save_plugin_options()
            self.save_alert_settings()
            entry_beef_hook = self.gtk_builder_get('entry_server_beef_hook')
            self.application.rpc('config/set', {
                'beef.hook_url':
                entry_beef_hook.get_property('text').strip()
            })
            if graphs.has_matplotlib:
                self._finialize_settings_dashboard()
        self.dialog.destroy()
        return response

    def save_proxy_settings(self):
        proxy_url = urllib.parse.urlparse(
            self.gtk_builder_get('entry_proxy_url').get_text().strip())
        proxy_username = self.gtk_builder_get(
            'entry_proxy_username').get_text().strip()
        proxy_password = self.gtk_builder_get(
            'entry_proxy_password').get_text().strip()
        if not proxy_url.geturl():
            self.config['proxy.url'] = None
            os.environ.pop('HTTP_PROXY', None)
            os.environ.pop('HTTPS_PROXY', None)
            return
        if not (proxy_url.hostname and proxy_url.scheme):
            gui_utilities.show_dialog_warning(
                'Invalid Proxy Settings', self.parent,
                'The proxy url you have submitted is not valid.')
            return
        try:
            proxy_url.port
        except ValueError:
            gui_utilities.show_dialog_warning(
                'Invalid Proxy Settings', self.parent,
                'The port must be an integer between 1-65535 inclusive.')
            return
        netloc = proxy_url.netloc
        if proxy_username:
            netloc = '{}:{}@{}'.format(proxy_username, proxy_password,
                                       proxy_url.netloc)
        formatted_proxy_url = urllib.parse.urlunparse(
            (proxy_url.scheme, netloc, proxy_url.path, '', '', ''))
        self.config['proxy.url'] = formatted_proxy_url
        os.environ['HTTP_PROXY'] = formatted_proxy_url
        os.environ['HTTPS_PROXY'] = formatted_proxy_url

    def save_plugin_options(self):
        for name, option_widgets in self._plugin_option_widgets.items():
            if name not in self.config['plugins']:
                self.config['plugins'][name] = {}
            plugin_config = self.config['plugins'][
                name]  # use or instead of get incase the value is actually None
            for option_name, option_widget in option_widgets.items():
                plugin_config[
                    option_name] = option_widget.option.get_widget_value(
                        option_widget.widget)
        checkbutton = self.gtk_builder_get(
            'checkbutton_pip_install_dependencies')
        self.config[
            'plugins.pip.install_dependencies'] = checkbutton.get_active()

    def save_alert_settings(self):
        email_address = gui_utilities.gobject_get_value(
            self.gobjects['entry_email_address'])
        phone_number = gui_utilities.gobject_get_value(
            self.gobjects['entry_sms_phone_number'])
        sms_carrier = gui_utilities.gobject_get_value(
            self.gobjects['combobox_sms_carrier'])
        server_user = self.application.server_user
        if email_address and not utilities.is_valid_email_address(
                email_address):
            gui_utilities.show_dialog_warning(
                'Invalid Email Address', self.parent,
                'The email address you have entered is not valid.')
            return
        if phone_number:
            phone_number = ''.join(d for d in phone_number
                                   if d in string.digits)
            if len(phone_number) > 11:
                gui_utilities.show_dialog_warning(
                    'Invalid Phone Number', self.parent,
                    'The phone number must not contain more than 11 digits')
                return
        email_address = utilities.nonempty_string(email_address)
        phone_number = utilities.nonempty_string(phone_number)
        sms_carrier = utilities.nonempty_string(sms_carrier)
        self.application.rpc(
            'db/table/set', 'users', server_user.id,
            ('email_address', 'phone_number', 'phone_carrier'),
            (email_address, phone_number, sms_carrier))
Beispiel #23
0
class CompanyEditorDialog(gui_utilities.GladeGObject):
    """
	Display a dialog which can be used to edit the various fields associated
	with a company object.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=(resources.CompanyEditorGrid(
            gui_utilities.GladeProxyDestination(
                widget='box_company',
                method='pack_start',
                kwargs=dict(expand=True, fill=True, padding=0))),
                  'combobox_company_existing'))
    top_gobject = 'dialog'
    objects_persist = False

    def __init__(self, *args, **kwargs):
        super(CompanyEditorDialog, self).__init__(*args, **kwargs)
        self._company_info_changed = False
        self._last_company_id = None
        self._set_comboboxes()
        self.gobjects['combobox_company_industry'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_industry'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_name'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_description'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_url_main'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_url_email'].connect(
            'changed', self.signal_editable_changed)
        self.gobjects['entry_company_url_remote_access'].connect(
            'changed', self.signal_editable_changed)

    def _set_comboboxes(self):
        """Set up all the comboboxes and load the data for their models."""
        renderer = resources.renderer_text_desc
        rpc = self.application.rpc
        for tag_name, tag_table in (('company_existing', 'companies'),
                                    ('company_industry', 'industries')):
            combobox = self.gobjects['combobox_' + tag_name]
            model = combobox.get_model()
            if model is None:
                combobox.pack_start(renderer, True)
                combobox.add_attribute(renderer, 'text', 2)
            combobox.set_model(rpc.get_tag_model(tag_table, model=model))

    def _get_company_info(self, company_id):
        company = self.application.rpc.graphql(
            """\
		query getCompany($id: String!) {
			db {
				company(id: $id) {
					id
					description
					name
					industryId
					urlMain
					urlEmail
					urlRemoteAccess
				}
			}
		}""", {'id': company_id})['db']['company']
        combobox = self.gobjects['combobox_company_industry']
        if company['industryId'] is None:
            combobox.set_active_iter(None)
            combobox.get_child().set_text('')
        else:
            combobox.set_active_iter(
                gui_utilities.gtk_list_store_search(combobox.get_model(),
                                                    company['industryId']))
        self.gobjects['entry_company_name'].set_text(company['name'])
        self.gobjects['entry_company_description'].set_text(
            company['description'] or '')
        self.gobjects['entry_company_url_main'].set_text(company['urlMain']
                                                         or '')
        self.gobjects['entry_company_url_email'].set_text(company['urlEmail']
                                                          or '')
        self.gobjects['entry_company_url_remote_access'].set_text(
            company['urlRemoteAccess'] or '')

    def _get_tag_from_combobox(self, combobox, db_table):
        model = combobox.get_model()
        model_iter = combobox.get_active_iter()
        if model_iter is not None:
            return model.get_value(model_iter, 0)
        campaign_type = combobox.get_child().get_text().strip()
        if not campaign_type:
            return
        model_iter = gui_utilities.gtk_list_store_search(model,
                                                         campaign_type,
                                                         column=1)
        if model_iter is None:
            return self.application.rpc('db/table/insert', db_table, 'name',
                                        campaign_type)
        return model.get_value(model_iter, 0)

    def _set_company_info(self, company_id):
        company_name = self.get_entry_value('company_name')
        company_description = self.get_entry_value('company_description')
        self.application.rpc.remote_table_row_set(
            'companies', company_id, {
                'name':
                company_name,
                'description':
                company_description,
                'industry_id':
                self._get_tag_from_combobox(
                    self.gobjects['combobox_company_industry'], 'industries'),
                'url_main':
                self.get_entry_value('company_url_main'),
                'url_email':
                self.get_entry_value('company_url_email'),
                'url_remote_access':
                self.get_entry_value('company_url_remote_access')
            })
        model = self.gobjects['combobox_company_existing'].get_model()
        model_iter = gui_utilities.gtk_list_store_search(model, company_id)
        model[model_iter][1] = company_name
        model[model_iter][2] = company_description

    def _should_set_company_info(self):
        if self._last_company_id is None:
            return False
        if not self._company_info_changed:
            return False
        return gui_utilities.show_dialog_yes_no(
            'Update Company Info?', self.parent,
            'Do you want to save the changes to the company information?')

    def interact(self):
        self.dialog.show_all()
        response = self.dialog.run()
        if response == Gtk.ResponseType.APPLY and self._should_set_company_info(
        ):
            self._set_company_info(self._last_company_id)
        self.dialog.destroy()

    def signal_combobox_company_changed(self, _):
        if self._should_set_company_info():
            self._set_company_info(self._last_company_id)

        combobox = self.gobjects['combobox_company_existing']
        model = combobox.get_model()
        company_id = model.get_value(combobox.get_active_iter(), 0)
        self._last_company_id = company_id
        self._get_company_info(company_id)
        self._company_info_changed = False

    def signal_editable_changed(self, _):
        if self._last_company_id is None:
            return
        self._company_info_changed = True
class CampaignViewGenericTableTab(CampaignViewGenericTab):
	"""
	This object is meant to be subclassed by tabs which will display
	campaign information of different types from specific database
	tables. The data in this object is refreshed when multiple events
	occur and it uses an internal timer to represent the last time the
	data was refreshed.
	"""
	dependencies = gui_utilities.GladeDependencies(
		children=(
			'button_refresh',
			'treeview_campaign'
		)
	)
	remote_table_name = ''
	"""The database table represented by this tab."""
	view_columns = ()
	"""The dictionary map of column numbers to column names starting at column 1."""
	def __init__(self, *args, **kwargs):
		super(CampaignViewGenericTableTab, self).__init__(*args, **kwargs)
		treeview = self.gobjects['treeview_campaign']
		self.treeview_manager = managers.TreeViewManager(
			treeview,
			selection_mode=Gtk.SelectionMode.MULTIPLE,
			cb_delete=self._prompt_to_delete_row,
			cb_refresh=self.load_campaign_information
		)
		self.treeview_manager.set_column_titles(self.view_columns, column_offset=1)
		self.popup_menu = self.treeview_manager.get_popup_menu()
		"""The :py:class:`Gtk.Menu` object which is displayed when right-clicking in the view area."""

	def _prompt_to_delete_row(self, treeview, selection):
		if isinstance(self.loader_thread, threading.Thread) and self.loader_thread.is_alive():
			gui_utilities.show_dialog_warning('Can Not Delete Rows While Loading', self.parent)
			return
		(model, tree_paths) = selection.get_selected_rows()
		if not tree_paths:
			return
		tree_iters = map(model.get_iter, tree_paths)
		row_ids = [model.get_value(ti, 0) for ti in tree_iters]
		if len(row_ids) == 1:
			message = 'Delete This Row?'
		else:
			message = "Delete These {0:,} Rows?".format(len(row_ids))
		if not gui_utilities.show_dialog_yes_no(message, self.parent, 'This information will be lost.'):
			return
		if len(row_ids) == 1:
			self.rpc('db/table/delete', self.remote_table_name, row_ids[0])
		else:
			self.rpc('db/table/delete/multi', self.remote_table_name, row_ids)
		self.load_campaign_information()

	def format_row_data(self, row):
		"""
		This method is overridden by subclasses to format the raw row
		data returned from the server. The length of the list must equal
		the number of columns in the table. This method is called for
		each row in the remote table by the loader thread.

		:return: The formated row data.
		:rtype: list
		"""
		raise NotImplementedError()

	def format_cell_data(self, cell_data):
		"""
		This method provides formatting to the individual cell values returned
		from the :py:meth:`.format_row_data` function. Values are converted into
		a format suitable for reading.

		:param cell: The value to format.
		:return: The formatted cell value.
		:rtype: str
		"""
		if isinstance(cell_data, datetime.datetime):
			cell_data = utilities.datetime_utc_to_local(cell_data)
			return utilities.format_datetime(cell_data)
		elif cell_data is None:
			return ''
		return str(cell_data)

	def load_campaign_information(self, force=True):
		"""
		Load the necessary campaign information from the remote server.
		Unless *force* is True, the
		:py:attr:`~.CampaignViewGenericTab.last_load_time` is compared
		with the :py:attr:`~.CampaignViewGenericTab.refresh_frequency` to
		check if the information is stale. If the local data is not stale,
		this function will return without updating the table.

		:param bool force: Ignore the load life time and force loading the remote data.
		"""
		if not force and ((time.time() - self.last_load_time) < self.refresh_frequency):
			return
		if isinstance(self.loader_thread, threading.Thread) and self.loader_thread.is_alive():
			return
		self.loader_thread_lock.acquire()
		treeview = self.gobjects['treeview_campaign']
		store = treeview.get_model()
		if store == None:
			store_columns = [str]
			for _ in range(len(self.view_columns)):
				store_columns.append(str)
			store = Gtk.ListStore(*store_columns)
			treeview.set_model(store)
		else:
			store.clear()
		self.loader_thread = threading.Thread(target=self.loader_thread_routine, args=(store,))
		self.loader_thread.daemon = True
		self.loader_thread.start()
		self.loader_thread_lock.release()
		return

	def loader_thread_routine(self, store):
		"""
		The loading routine to be executed within a thread.

		:param store: The store object to place the new data.
		:type store: :py:class:`Gtk.ListStore`
		"""
		gui_utilities.glib_idle_add_wait(lambda: self.gobjects['treeview_campaign'].set_property('sensitive', False))
		for row in self.rpc.remote_table(self.remote_table_name, query_filter={'campaign_id': self.config['campaign_id']}):
			if self.is_destroyed.is_set():
				break
			if self.rpc is None:
				break
			row_data = self.format_row_data(row)
			if row_data is None:
				self.rpc('db/table/delete', self.remote_table_name, row.id)
				continue
			row_data = list(map(self.format_cell_data, row_data))
			row_data.insert(0, str(row.id))
			gui_utilities.glib_idle_add_wait(store.append, row_data)
		if self.is_destroyed.is_set():
			return
		gui_utilities.glib_idle_add_wait(lambda: self.gobjects['treeview_campaign'].set_property('sensitive', True))
		self.last_load_time = time.time()

	def signal_button_clicked_export(self, button):
		self.export_table_to_csv()

	def export_table_to_csv(self):
		"""Export the data represented by the view to a CSV file."""
		if not self.loader_thread_lock.acquire(False) or (isinstance(self.loader_thread, threading.Thread) and self.loader_thread.is_alive()):
			gui_utilities.show_dialog_warning('Can Not Export Rows While Loading', self.parent)
			return
		dialog = extras.FileChooserDialog('Export Data', self.parent)
		file_name = self.config['campaign_name'] + '.csv'
		response = dialog.run_quick_save(file_name)
		dialog.destroy()
		if not response:
			self.loader_thread_lock.release()
			return
		destination_file = response['target_path']
		store = self.gobjects['treeview_campaign'].get_model()
		columns = dict(enumerate(('UID',) + self.view_columns))
		export.liststore_to_csv(store, destination_file, columns)
		self.loader_thread_lock.release()

	def export_table_to_xlsx_worksheet(self, worksheet, title_format):
		"""
		Export the data represented by the view to an XLSX worksheet.

		:param worksheet: The destination sheet for the store's data.
		:type worksheet: :py:class:`xlsxwriter.worksheet.Worksheet`
		:param title_format: The formatting to use for the title row.
		:type title_format: :py:class:`xlsxwriter.format.Format`
		"""
		if not self.loader_thread_lock.acquire(False) or (isinstance(self.loader_thread, threading.Thread) and self.loader_thread.is_alive()):
			gui_utilities.show_dialog_warning('Can Not Export Rows While Loading', self.parent)
			return
		store = self.gobjects['treeview_campaign'].get_model()
		columns = dict(enumerate(('UID',) + self.view_columns))
		export.liststore_to_xlsx_worksheet(store, worksheet, columns, title_format)
		self.loader_thread_lock.release()
Beispiel #25
0
class MainMenuBar(gui_utilities.GladeGObject):
    """
	The main menu bar for the primary application window. This configures any
	optional menu items as well as handles all the menu item signals
	appropriately.
	"""
    dependencies = gui_utilities.GladeDependencies(
        top_level=('StockDeleteImage', 'StockEditImage', 'StockHelpImage',
                   'StockPropertiesImage', 'StockStopImage'))
    top_gobject = 'menubar'

    def __init__(self, application, window):
        utilities.assert_arg_type(application, Gtk.Application, arg_pos=1)
        utilities.assert_arg_type(window, MainAppWindow, arg_pos=2)
        super(MainMenuBar, self).__init__(application)
        self.window = weakref.proxy(window)
        self._add_accelerators()
        graphs_menu_item = self.gtk_builder_get('menuitem_tools_create_graph')
        if graphs.has_matplotlib:
            graphs_submenu = Gtk.Menu.new()
            for graph_name in graphs.get_graphs():
                graph = graphs.get_graph(graph_name)
                menu_item = Gtk.MenuItem.new_with_label(graph.name_human)
                menu_item.connect(
                    'activate', self.signal_activate_tools_show_campaign_graph,
                    graph_name)
                graphs_submenu.append(menu_item)
            graphs_menu_item.set_submenu(graphs_submenu)
            graphs_menu_item.show_all()
        else:
            graphs_menu_item.set_sensitive(False)

    def _add_accelerators(self):
        accelerators = (('file_open', Gdk.KEY_o,
                         Gdk.ModifierType.CONTROL_MASK),
                        ('file_quit', Gdk.KEY_q,
                         Gdk.ModifierType.CONTROL_MASK),
                        ('tools_rpc_terminal', Gdk.KEY_F1,
                         Gdk.ModifierType.CONTROL_MASK),
                        ('tools_sftp_client', Gdk.KEY_F2,
                         Gdk.ModifierType.CONTROL_MASK))
        for menu_name, key, modifier in accelerators:
            menu_item = self.gtk_builder_get('menuitem_' + menu_name)
            menu_item.add_accelerator('activate', self.window.accel_group, key,
                                      modifier, Gtk.AccelFlags.VISIBLE)

    def signal_activate_edit_configure_campaign(self, _):
        self.application.campaign_configure()

    def signal_activate_edit_delete_campaign(self, _):
        if not gui_utilities.show_dialog_yes_no(
                'Delete This Campaign?', self.application.get_active_window(),
                'This action is irreversible, all campaign data will be lost.'
        ):
            return
        self.application.emit('campaign-delete', self.config['campaign_id'])

    def signal_activate_edit_preferences(self, _):
        self.application.show_preferences()

    def signal_activate_edit_stop_service(self, _):
        self.application.stop_remote_service()

    def signal_activate_edit_companies(self, _):
        dialogs.CompanyEditorDialog(self.application).interact()

    def signal_activate_edit_tags(self, _):
        dialogs.TagEditorDialog(self.application).interact()

    def signal_activate_export_campaign_xlsx(self, _):
        self.window.export_campaign_xlsx()

    def signal_activate_export_campaign_xml(self, _):
        self.window.export_campaign_xml()

    def signal_activate_export_message_data(self, _):
        self.window.export_message_data()

    def signal_activate_export_credentials_csv(self, _):
        campaign_tab = self.window.tabs['campaign']
        credentials_tab = campaign_tab.tabs['credentials']
        credentials_tab.export_table_to_csv()

    def signal_activate_export_credentials_msf_txt(self, _):
        dialog = extras.FileChooserDialog('Export Credentials',
                                          self.application.get_active_window())
        file_name = self.config['campaign_name'] + '.txt'
        response = dialog.run_quick_save(file_name)
        dialog.destroy()
        if not response:
            return
        destination_file = response['target_path']
        export.campaign_credentials_to_msf_txt(self.application.rpc,
                                               self.config['campaign_id'],
                                               destination_file)

    def signal_activate_export_messages_csv(self, _):
        campaign_tab = self.window.tabs['campaign']
        messages_tab = campaign_tab.tabs['messages']
        messages_tab.export_table_to_csv()

    def signal_activate_export_visits_csv(self, _):
        campaign_tab = self.window.tabs['campaign']
        visits_tab = campaign_tab.tabs['visits']
        visits_tab.export_table_to_csv()

    def signal_activate_export_visits_geojson(self, _):
        self.window.export_campaign_visit_geojson()

    def signal_activate_import_message_data(self, _):
        self.window.import_message_data()

    def signal_activate_import_campaign_xml(self, _):
        campaign_import.ImportCampaignWindow(self.application)

    def signal_activate_show_campaign_selection(self, _):
        self.application.show_campaign_selection()

    def signal_activate_quit(self, _):
        self.application.quit(optional=True)

    def signal_activate_tools_rpc_terminal(self, _):
        rpc_terminal.RPCTerminal(self.application)

    def signal_activate_tools_clone_page(self, _):
        dialogs.ClonePageDialog(self.application).interact()

    def signal_activate_tools_compare_campaigns(self, _):
        compare_campaigns.CampaignCompWindow(self.application)

    def signal_activate_tools_manage_plugins(self, _):
        plugin_manager.PluginManagerWindow(self.application)

    def signal_activate_tools_sftp_client(self, _):
        self.application.emit('sftp-client-start')

    def signal_activate_tools_show_campaign_graph(self, _, graph_name):
        self.application.show_campaign_graph(graph_name)

    def signal_activate_help_about(self, _):
        dialogs.AboutDialog(self.application).interact()

    def signal_activate_help_templates(self, _):
        utilities.open_uri(
            'https://github.com/securestate/king-phisher-templates')

    def signal_activate_help_wiki(self, _):
        utilities.open_uri('https://github.com/securestate/king-phisher/wiki')
Beispiel #26
0
class PluginManagerWindow(gui_utilities.GladeGObject):
    """
	The window which allows the user to selectively enable and disable plugins
	for the client application. This also handles configuration changes, so the
	enabled plugins will persist across application runs.
	"""
    dependencies = gui_utilities.GladeDependencies(children=(
        'expander_info', 'grid_catalog_repo_info', 'grid_plugin_info',
        'label_catalog_repo_info_description',
        'label_catalog_repo_info_for_description',
        'label_catalog_repo_info_for_maintainers',
        'label_catalog_repo_info_homepage',
        'label_catalog_repo_info_maintainers', 'label_catalog_repo_info_title',
        'label_plugin_info_authors', 'label_plugin_info_compatible',
        'label_plugin_info_description', 'label_plugin_info_for_classifiers',
        'label_plugin_info_for_compatible', 'label_plugin_info_for_references',
        'label_plugin_info_homepage', 'label_plugin_info_title',
        'label_plugin_info_version', 'listbox_plugin_info_classifiers',
        'listbox_plugin_info_references', 'menubutton_plugin_info',
        'paned_plugins', 'scrolledwindow_plugins', 'stack_info',
        'treeview_plugins', 'textview_plugin_info', 'viewport_info',
        'statusbar'))
    top_gobject = 'window'

    # methods defined within this class that are suffixed with _tsafe are safe
    # to be called from a non-GUI thread and by extension only call fellow
    # _tsafe methods
    def __init__(self, *args, **kwargs):
        super(PluginManagerWindow, self).__init__(*args, **kwargs)
        self.catalog_plugins = plugins.ClientCatalogManager(
            self.application.user_data_path)
        self.plugin_path = os.path.join(self.application.user_data_path,
                                        'plugins')
        self.status_bar = self.gobjects['statusbar']
        self._installed_plugins_treeview_tracker = None
        """
		This is used to track and make sure all plugins make it into the
		treeview. It is set each time catalogs are loaded or refreshed. Once the
		loading operation is complete, plugins that remain were not loaded due
		their data (repo or id) missing from the catalog, likely due to it
		having been removed.
		"""
        self._worker_thread = None
        self._worker_thread_start(self._load_catalogs_tsafe)
        self.__load_errors = {}
        self.__installing_plugin = None

        tvm = managers.TreeViewManager(self.gobjects['treeview_plugins'])
        toggle_renderer_enable = Gtk.CellRendererToggle()
        toggle_renderer_enable.connect('toggled',
                                       self.signal_renderer_toggled_enable)
        toggle_renderer_install = Gtk.CellRendererToggle()
        toggle_renderer_install.connect('toggled',
                                        self.signal_renderer_toggled_install)
        tvm.set_column_titles(
            ('Installed', 'Enabled', 'Title', 'Compatible', 'Version'),
            column_offset=1,
            renderers=(toggle_renderer_install, toggle_renderer_enable,
                       Gtk.CellRendererText(), Gtk.CellRendererText(),
                       Gtk.CellRendererText()))
        tvm.column_views['Enabled'].set_cell_data_func(
            toggle_renderer_enable, self._toggle_enabled_cell_data_func)
        tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable,
                                                  'visible', 6)
        tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable,
                                                  'sensitive', 1)
        tvm.column_views['Installed'].set_cell_data_func(
            toggle_renderer_install, self._toggle_install_cell_data_func)
        tvm.column_views['Installed'].add_attribute(toggle_renderer_install,
                                                    'visible', 7)
        tvm.column_views['Installed'].add_attribute(toggle_renderer_install,
                                                    'sensitive', 8)
        self._model = Gtk.TreeStore(str, bool, bool, str, str, str, bool, bool,
                                    bool, str)
        self._model.set_sort_column_id(3, Gtk.SortType.ASCENDING)
        self.gobjects['treeview_plugins'].set_model(self._model)

        self._tv_popup_menu = managers.MenuManager(tvm.get_popup_menu())
        self._tv_popup_menu.append_item(Gtk.SeparatorMenuItem())
        self._tv_popup_menu.append('Reload',
                                   self.signal_popup_menu_activate_reload)
        self._tv_popup_menu.append('Reload All',
                                   self.signal_popup_menu_activate_reload_all)
        self._tv_popup_menu.append_item(Gtk.SeparatorMenuItem())
        self._tv_popup_menu.append(
            'Show Documentation',
            self.signal_popup_menu_activate_show_documentation)
        self._tv_popup_menu.append('Update',
                                   self.signal_popup_menu_activate_update)

        self._info_popup_menu = managers.MenuManager()
        self._info_popup_menu.append('Reload',
                                     self.signal_popup_menu_activate_reload)
        self._info_popup_menu.append_item(Gtk.SeparatorMenuItem())
        self._info_popup_menu.append(
            'Show Documentation',
            self.signal_popup_menu_activate_show_documentation)
        self._info_popup_menu.append('Update',
                                     self.signal_popup_menu_activate_update)
        self.gobjects['menubutton_plugin_info'].set_popup(
            self._info_popup_menu.menu)

        self._update_status_bar('Loading...')
        self.window.show()
        paned = self.gobjects['paned_plugins']
        self._paned_offset = paned.get_allocation(
        ).height - paned.get_position()

    def __store_add_node(self, node, parent=None):
        """
		Add a :py:class:`._ModelNode` to :py:attr:`._model`, recursively adding
		child :py:class:`._ModelNode` or :py:class:`._ModelNamedRow` instances as
		necessary. This is *not* tsafe.

		:param node: The node to add to the TreeView model.
		:type node: :py:class:`._ModelNode`
		:param parent: An optional parent for the node, used for recursion.
		"""
        row = self._model.append(parent, node.row)
        for child in node.children:
            if isinstance(child, _ModelNode):
                self.__store_add_node(child, parent=row)
            elif isinstance(child, _ModelNamedRow):
                self._model.append(row, child)
            else:
                raise TypeError('unsupported node child type')

    def _add_catalog_to_tree_tsafe(self, catalog):
        """
		Create a :py:class:`._ModelNode` instance to representing the catalog, its
		data and add it to the TreeView model.

		:param catalog: The catalog to add to the TreeView.
		:type catalog: :py:class:`.Catalog`
		"""
        catalog_node = _ModelNode(id=catalog.id,
                                  installed=None,
                                  enabled=True,
                                  title=catalog.id,
                                  compatibility=None,
                                  version=None,
                                  visible_enabled=False,
                                  visible_installed=False,
                                  sensitive_installed=False,
                                  type=_ROW_TYPE_CATALOG)
        for repo in catalog.repositories.values():
            repo_node = _ModelNode(id=repo.id,
                                   installed=None,
                                   enabled=True,
                                   title=repo.title,
                                   compatibility=None,
                                   version=None,
                                   visible_enabled=False,
                                   visible_installed=False,
                                   sensitive_installed=False,
                                   type=_ROW_TYPE_REPOSITORY)
            catalog_node.children.append(repo_node)

            plugin_collection = self.catalog_plugins.get_collection(
                catalog.id, repo.id)
            for plugin_info in plugin_collection.values():
                installed = False
                enabled = False
                plugin_name = plugin_info['name']
                install_src = self.config['plugins.installed'].get(plugin_name)
                if install_src and repo.id == install_src[
                        'repo_id'] and catalog.id == install_src['catalog_id']:
                    installed = True
                    # plugin was added to treeview so it is removed from the temporary tracking dict
                    self._installed_plugins_treeview_tracker.pop(plugin_name)
                    enabled = plugin_name in self.config['plugins.enabled']
                repo_node.children.append(
                    _ModelNamedRow(
                        id=plugin_name,
                        installed=installed,
                        enabled=enabled,
                        title=plugin_info['title'],
                        compatibility='Yes'
                        if self.catalog_plugins.is_compatible(
                            catalog.id, repo.id, plugin_name) else 'No',
                        version=plugin_info['version'],
                        visible_enabled=True,
                        visible_installed=True,
                        sensitive_installed=self.catalog_plugins.is_compatible(
                            catalog.id, repo.id, plugin_name),
                        type=_ROW_TYPE_PLUGIN))
        gui_utilities.glib_idle_add_once(self.__store_add_node, catalog_node)

    def _get_plugin_model_parents(self, plugin_model_row):
        return _ModelNamedRow(*plugin_model_row.parent), _ModelNamedRow(
            *plugin_model_row.parent.parent)

    def _on_plugin_load_error_tsafe(self, name, error):
        # WARNING: this may not be called from the GUI thread
        self.__load_errors[name] = (error,
                                    traceback.format_exception(*sys.exc_info(),
                                                               limit=5))

    def _plugin_disable(self, model_row):
        named_row = _ModelNamedRow(*model_row)
        self.application.plugin_manager.disable(named_row.id)
        self.config['plugins.enabled'].remove(named_row.id)
        model_row[_ModelNamedRow._fields.index('enabled')] = False

    def _plugin_enable(self, model_row):
        named_row = _ModelNamedRow(*model_row)
        pm = self.application.plugin_manager
        if not pm.loaded_plugins[named_row.id].is_compatible:
            gui_utilities.show_dialog_error('Incompatible Plugin', self.window,
                                            'This plugin is not compatible.')
            return
        if not pm.enable(named_row.id):
            return
        self._set_model_item(model_row.path, 'enabled', True)
        self.config['plugins.enabled'].append(named_row.id)

    def _plugin_install(self, model_row):
        if not self._worker_thread_is_ready:
            # check it here to fail fast, then self._worker_thread_start checks it again later
            self._show_dialog_busy()
            return
        named_row = _ModelNamedRow(*model_row)
        repo_model, catalog_model = self._get_plugin_model_parents(model_row)
        if named_row.id in self.config['plugins.installed']:
            plugin_src = self.config['plugins.installed'].get(named_row.id)
            if plugin_src != {
                    'catalog_id': catalog_model.id,
                    'repo_id': repo_model.id,
                    'plugin_id': named_row.id
            }:
                window_question = 'A plugin with this name is already installed from another\nrepository. Do you want to replace it with this one?'
                if not gui_utilities.show_dialog_yes_no(
                        'Plugin installed from another source', self.window,
                        window_question):
                    return
                if not self._remove_matching_plugin(named_row, plugin_src):
                    self.logger.warning(
                        "failed to uninstall plugin {0}".format(named_row.id))
                    return
        self._worker_thread_start(self._plugin_install_tsafe, catalog_model,
                                  repo_model, model_row, named_row)

    def _plugin_install_tsafe(self, catalog_model, repo_model, model_row,
                              named_row):
        self.__installing_plugin = named_row.id
        self._update_status_bar_tsafe("Installing plugin {}...".format(
            named_row.title))
        _show_dialog_error_tsafe = functools.partial(
            gui_utilities.glib_idle_add_once, gui_utilities.show_dialog_error,
            'Failed To Install', self.window)
        try:
            self.catalog_plugins.install_plugin(catalog_model.id,
                                                repo_model.id, named_row.id,
                                                self.plugin_path)
        except requests.exceptions.ConnectionError:
            self.logger.warning("failed to download plugin {}".format(
                named_row.id))
            _show_dialog_error_tsafe(
                "Failed to download {} plugin, check your internet connection."
                .format(named_row.id))
            self._update_status_bar_tsafe(
                "Installing plugin {} failed.".format(named_row.title))
            return
        except Exception:
            self.logger.warning("failed to install plugin {}".format(
                named_row.id),
                                exc_info=True)
            _show_dialog_error_tsafe("Failed to install {} plugin.".format(
                named_row.id))
            self._update_status_bar_tsafe(
                "Installing plugin {} failed.".format(named_row.title))
            return
        finally:
            self.__installing_plugin = None

        self.config['plugins.installed'][named_row.id] = {
            'catalog_id': catalog_model.id,
            'repo_id': repo_model.id,
            'plugin_id': named_row.id
        }
        self.logger.info(
            "installed plugin {} from catalog:{}, repository:{}".format(
                named_row.id, catalog_model.id, repo_model.id))
        self._reload_plugin_tsafe(model_row, named_row)
        gui_utilities.glib_idle_add_once(self.__plugin_install_post,
                                         catalog_model, repo_model, model_row,
                                         named_row)

    def __plugin_install_post(self, catalog_model, repo_model, model_row,
                              named_row):
        # handles GUI related updates after data has been fetched from the internet
        if model_row.path is not None:
            self._set_model_item(model_row.path, 'installed', True)
            self._set_model_item(
                model_row.path, 'version',
                self.catalog_plugins.get_collection(
                    catalog_model.id, repo_model.id)[named_row.id]['version'])
            if self._selected_model_row.path == model_row.path:
                self._popup_menu_refresh(model_row)
        self._update_status_bar("Installing plugin {} completed.".format(
            named_row.title))

    def _plugin_uninstall(self, model_row):
        plugin_id = _ModelNamedRow(*model_row).id
        if not self.application.plugin_manager.uninstall(plugin_id):
            return False
        del self.config['plugins.installed'][plugin_id]
        if model_row.parent and model_row.parent[_ModelNamedRow._fields.index(
                'id')] == _LOCAL_REPOSITORY_ID:
            del self._model[model_row.path]
        else:
            self._set_model_item(model_row.path, 'installed', False)
        self.logger.info(
            "successfully uninstalled plugin {0}".format(plugin_id))
        return True

    def _popup_menu_refresh(self, model_row):
        named_row = _ModelNamedRow(*model_row)
        sensitive = named_row.type == _ROW_TYPE_PLUGIN and named_row.installed
        self._info_popup_menu['Show Documentation'].set_property(
            'sensitive', sensitive)
        self._tv_popup_menu['Show Documentation'].set_property(
            'sensitive', sensitive)
        sensitive = named_row.type == _ROW_TYPE_PLUGIN and named_row.installed and named_row.sensitive_installed
        self._info_popup_menu['Update'].set_property('sensitive', sensitive)
        self._tv_popup_menu['Update'].set_property('sensitive', sensitive)

    def _reload(self):
        model_row = self._selected_model_row
        named_row = _ModelNamedRow(*model_row)
        if named_row.type == _ROW_TYPE_CATALOG:
            self._worker_thread_start(self._reload_catalog_tsafe, model_row,
                                      named_row)
        elif named_row.type == _ROW_TYPE_REPOSITORY:
            # this just reloads the entire parent catalog, individual repositories
            # can not be reloaded at this time
            parent_model_row = model_row.parent
            parent_named_row = _ModelNamedRow(*parent_model_row)
            if parent_named_row.type != _ROW_TYPE_CATALOG:
                self.logger.warning(
                    'repository treeview row\'s parent is not a catalog')
                return
            self._worker_thread_start(self._reload_catalog_tsafe,
                                      parent_model_row, parent_named_row)
        elif named_row.type == _ROW_TYPE_PLUGIN:
            if not named_row.installed:
                return
            self._worker_thread_start(self._reload_plugin_tsafe, model_row,
                                      named_row)
        else:
            self.logger.warning('reload selected for an unsupported row type')

    def _reload_catalog_tsafe(self, model_row, named_row):
        self._update_status_bar_tsafe('Reloading catalog...')
        self._model.remove(model_row.iter)
        if named_row.id == _LOCAL_REPOSITORY_ID:
            self._load_catalog_local_tsafe()
        else:
            catalog_url = self.catalog_plugins.get_cache().get_catalog_by_id(
                named_row.id)['url']
            if catalog_url:
                self._load_catalog_from_url_tsafe(catalog_url)
        self._update_status_bar_tsafe('Reloading catalog... completed.')

    def _reload_plugin_tsafe(self, model_row, named_row, enabled=None):
        self._update_status_bar_tsafe('Reloading plugin...')
        pm = self.application.plugin_manager
        if enabled is None:
            enabled = named_row.id in pm.enabled_plugins
        pm.unload(named_row.id)
        try:
            klass = pm.load(named_row.id, reload_module=True)
        except Exception as error:
            self._on_plugin_load_error_tsafe(named_row.id, error)
            klass = None
        else:
            if enabled:
                pm.enable(named_row.id)
            self.__load_errors.pop(named_row.id, None)
        gui_utilities.glib_idle_add_once(self.__reload_plugin_post, model_row,
                                         named_row, klass)

    def __reload_plugin_post(self, model_row, named_row, klass=None):
        if model_row.path is not None:
            if named_row.id == self._selected_named_row.id:
                self._set_info(model_row)
            if klass is None:
                self._set_model_item(
                    model_row.path, 'title',
                    "{0} (Reload Failed)".format(named_row.id))
            else:
                self._set_model_item(model_row.path, 'title', klass.title)
                self._set_model_item(model_row.path, 'compatibility',
                                     'Yes' if klass.is_compatible else 'No')
                self._set_model_item(model_row.path, 'version', klass.version)
        self._update_status_bar('Reloading plugin... completed.')

    def _remove_matching_plugin(self, named_row, plugin_src):
        repo_model = None
        for catalog_model in self._model:
            catalog_id = _ModelNamedRow(*catalog_model).id
            if plugin_src and catalog_id == plugin_src['catalog_id']:
                repo_model = next(
                    (rm for rm in catalog_model.iterchildren()
                     if _ModelNamedRow(*rm).id == plugin_src['repo_id']), None)
                break
            elif plugin_src is None and catalog_id == _LOCAL_REPOSITORY_ID:
                # local installation acts as a pseudo-repository
                repo_model = catalog_model
                break
        if not repo_model:
            return False
        for plugin_model_row in repo_model.iterchildren():
            named_model = _ModelNamedRow(*plugin_model_row)
            if named_model.id != named_row.id:
                continue
            if named_model.enabled:
                self._plugin_disable(plugin_model_row)
            self._plugin_uninstall(plugin_model_row)
            return True
        return False

    @property
    def _selected_model_row(self):
        treeview = self.gobjects['treeview_plugins']
        selection = treeview.get_selection()
        if not selection.count_selected_rows():
            return None
        (model, tree_paths) = selection.get_selected_rows()
        return model[tree_paths[0]]

    @property
    def _selected_named_row(self):
        model_row = self._selected_model_row
        return _ModelNamedRow(*model_row) if model_row else None

    def _set_model_item(self, model_path, item, item_value):
        self._model[model_path][_ModelNamedRow._fields.index(
            item)] = item_value

    def _set_info(self, model_instance):
        named_model = _ModelNamedRow(*model_instance)
        stack = self.gobjects['stack_info']
        textview = self.gobjects['textview_plugin_info']
        buf = textview.get_buffer()
        buf.delete(buf.get_start_iter(), buf.get_end_iter())
        model_id = named_model.id
        if named_model.type == _ROW_TYPE_PLUGIN:
            if model_id in self.__load_errors:
                stack.set_visible_child(textview)
                self._set_info_plugin_error(model_instance)
            else:
                stack.set_visible_child(self.gobjects['grid_plugin_info'])
                self._set_info_plugin(model_instance)
        else:
            self._set_info_nonplugin(model_instance)

    def _set_info_nonplugin(self, model_instance):
        stack = self.gobjects['stack_info']
        stack.set_visible_child(self.gobjects['grid_catalog_repo_info'])
        named_model = _ModelNamedRow(*model_instance)
        obj_catalog = None

        # hide catalog repo labels
        self.gobjects['label_catalog_repo_info_maintainers'].set_property(
            'visible', False)
        self.gobjects['label_catalog_repo_info_for_maintainers'].set_property(
            'visible', False)
        self.gobjects['label_catalog_repo_info_description'].set_property(
            'visible', False)
        self.gobjects['label_catalog_repo_info_for_description'].set_property(
            'visible', False)
        self.gobjects['label_catalog_repo_info_homepage'].set_property(
            'visible', False)

        self.gobjects['label_catalog_repo_info_title'].set_text(
            named_model.title)
        if not named_model.id:
            return
        if named_model.type == _ROW_TYPE_CATALOG:
            obj = self.catalog_plugins.catalogs.get(named_model.id, None)
            if not obj:
                return
        else:
            obj_catalog = self.catalog_plugins.catalogs.get(
                _ModelNamedRow(*model_instance.parent).id, None)
            if not obj_catalog:
                return
            obj = self.catalog_plugins.catalogs[_ModelNamedRow(
                *model_instance.parent).id].repositories[named_model.id]

        maintainers = getattr(obj, 'maintainers',
                              getattr(obj_catalog, 'maintainers', None))
        if maintainers:
            self.gobjects['label_catalog_repo_info_maintainers'].set_text(
                '\n'.join(maintainers))
            self.gobjects['label_catalog_repo_info_maintainers'].set_property(
                'visible', True)
            self.gobjects[
                'label_catalog_repo_info_for_maintainers'].set_property(
                    'visible', True)
        if getattr(obj, 'description', None):
            self.gobjects['label_catalog_repo_info_description'].set_text(
                obj.description)
            self.gobjects['label_catalog_repo_info_description'].set_property(
                'visible', True)
            self.gobjects[
                'label_catalog_repo_info_for_description'].set_property(
                    'visible', True)
        if getattr(obj, 'homepage', None) or getattr(obj, 'url', None):
            url = getattr(obj, 'homepage', getattr(obj, 'url', None))
            self.gobjects['label_catalog_repo_info_homepage'].set_markup(
                "<a href=\"{0}\">Homepage</a>".format(
                    url.replace('"', '&quot;')))
            self.gobjects['label_catalog_repo_info_homepage'].set_property(
                'tooltip-text', url)
            self.gobjects['label_catalog_repo_info_homepage'].set_property(
                'visible', True)

    def _set_info_plugin(self, plugin_model):
        named_model = _ModelNamedRow(*plugin_model)
        pm = self.application.plugin_manager
        self._last_plugin_selected = plugin_model
        if named_model.id in pm.loaded_plugins:
            plugin = pm.loaded_plugins[named_model.id].metadata
            is_compatible = plugin['is_compatible']
        else:
            repo_model, catalog_model = self._get_plugin_model_parents(
                plugin_model)
            plugin = self.catalog_plugins.get_collection(
                catalog_model.id, repo_model.id)[named_model.id]
            is_compatible = self.catalog_plugins.is_compatible(
                catalog_model.id, repo_model.id, named_model.id)

        self.gobjects['label_plugin_info_title'].set_text(plugin['title'])
        self.gobjects['label_plugin_info_compatible'].set_text(
            'Yes' if is_compatible else 'No')
        self.gobjects['label_plugin_info_version'].set_text(plugin['version'])
        self.gobjects['label_plugin_info_authors'].set_text('\n'.join(
            plugin['authors']))
        self.gobjects['label_plugin_info_description'].set_text(
            plugin['description'])
        self._set_info_plugin_homepage_url(plugin['homepage'])
        self._set_info_plugin_reference_urls(plugin.get('reference_urls', []))
        self._set_info_plugin_classifiers(plugin.get('classifiers', []))

    def _set_info_plugin_classifiers(self, classifiers):
        label = self.gobjects['label_plugin_info_for_classifiers']
        listbox = self.gobjects['listbox_plugin_info_classifiers']
        gui_utilities.gtk_widget_destroy_children(listbox)
        if not classifiers:
            label.set_property('visible', False)
            listbox.set_property('visible', False)
            return
        label.set_property('visible', True)
        listbox.set_property('visible', True)
        for classifier in classifiers:
            label = Gtk.Label()
            label.set_markup(
                "<span font=\"smaller\"><tt>{0}</tt></span>".format(
                    saxutils.escape(classifier)))
            label.set_property('halign', Gtk.Align.START)
            label.set_property('use-markup', True)
            label.set_property('valign', Gtk.Align.START)
            label.set_property('visible', True)
            listbox.add(label)

    def _set_info_plugin_error(self, model_instance):
        id_ = _ModelNamedRow(*model_instance).id
        textview = self.gobjects['textview_plugin_info']
        buf = textview.get_buffer()
        exc, formatted_exc = self.__load_errors[id_]
        buf.insert(buf.get_end_iter(), "{0!r}\n\n".format(exc), -1)
        buf.insert(buf.get_end_iter(), ''.join(formatted_exc), -1)

    def _set_info_plugin_homepage_url(self, url=None):
        label_homepage = self.gobjects['label_plugin_info_homepage']
        if url is None:
            label_homepage.set_property('visible', False)
            return
        label_homepage.set_markup("<a href=\"{0}\">Homepage</a>".format(
            url.replace('"', '&quot;')))
        label_homepage.set_property('tooltip-text', url)
        label_homepage.set_property('visible', True)

    def _set_info_plugin_reference_urls(self, reference_urls):
        label = self.gobjects['label_plugin_info_for_references']
        listbox = self.gobjects['listbox_plugin_info_references']
        gui_utilities.gtk_widget_destroy_children(listbox)
        if not reference_urls:
            label.set_property('visible', False)
            listbox.set_property('visible', False)
            return
        label.set_property('visible', True)
        listbox.set_property('visible', True)
        for reference_url in reference_urls:
            label = Gtk.Label()
            label.connect('activate-link', self.signal_label_activate_link)
            label.set_markup("<a href=\"{0}\">{1}</a>".format(
                reference_url.replace('"', '&quot;'),
                saxutils.escape(reference_url)))
            label.set_property('halign', Gtk.Align.START)
            label.set_property('track-visited-links', False)
            label.set_property('use-markup', True)
            label.set_property('valign', Gtk.Align.START)
            label.set_property('visible', True)
            listbox.add(label)

    def _show_dialog_busy(self):
        gui_utilities.show_dialog_warning('Currently Busy', self.window,
                                          'An operation is already running.')

    def _show_dialog_error_tsafe(self, title, message):
        gui_utilities.glib_idle_add_once(gui_utilities.show_dialog_error,
                                         title, self.window, message)

    def _toggle_enabled_cell_data_func(self, column, cell, model, tree_iter,
                                       _):
        if model.get_value(tree_iter, 0) in self.__load_errors:
            cell.set_property('inconsistent', True)
        else:
            cell.set_property('inconsistent', False)

    def _toggle_install_cell_data_func(self, column, cell, model, tree_iter,
                                       _):
        cell.set_property(
            'inconsistent',
            model.get_value(tree_iter, 0) == self.__installing_plugin)

    def _update_status_bar(self, string_to_set):
        self.status_bar.pop(0)
        self.status_bar.push(0, string_to_set)

    def _update_status_bar_tsafe(self, string_to_set):
        gui_utilities.glib_idle_add_once(self._update_status_bar,
                                         string_to_set)

    def _worker_thread_start(self, target, *args, **kwargs):
        """
		Start a worker thread. This must only be called from the main GUI thread
		and *target* must be a tsafe method.
		"""
        if not self._worker_thread_is_ready:
            self._show_dialog_busy()
            self.logger.debug(
                'plugin manager worker thread is alive, can not start a new one'
            )
            return False
        self._worker_thread = utilities.Thread(target=target,
                                               args=args,
                                               kwargs=kwargs)
        self._worker_thread.start()
        return True

    @property
    def _worker_thread_is_ready(self):
        return self._worker_thread is None or not self._worker_thread.is_alive(
        )

    #
    # Catalog Loading Methods
    #
    # Each of these functions loads the catalog and handles add it to the
    # TreeView as necessary.
    #
    def _load_catalogs_tsafe(self, refresh=False):
        self._installed_plugins_treeview_tracker = copy.deepcopy(
            self.config['plugins.installed'])
        for plugin in list(self._installed_plugins_treeview_tracker.keys()):
            # Remove plugins already found to be locally installed.
            if not self._installed_plugins_treeview_tracker[plugin]:
                self._installed_plugins_treeview_tracker.pop(plugin)
        if refresh:
            gui_utilities.glib_idle_add_once(self._model.clear)
        expiration = datetime.timedelta(
            seconds=smoke_zephyr.utilities.parse_timespan(
                self.config.get('cache.age', '4h')))
        self._update_status_bar_tsafe('Loading, catalogs...')
        self._load_catalog_local_tsafe()
        catalog_cache = self.catalog_plugins.get_cache()
        now = datetime.datetime.utcnow()
        for catalog_url in self.config['catalogs']:
            catalog_cache_dict = catalog_cache.get_catalog_by_url(catalog_url)
            if not refresh and catalog_cache_dict and catalog_cache_dict[
                    'created'] + expiration > now:
                catalog = self._load_catalog_from_cache_tsafe(
                    catalog_cache_dict)
                if catalog is not None:
                    continue
                catalog_cache_dict = None
            self.logger.debug("downloading catalog: {}".format(catalog_url))
            self._update_status_bar_tsafe(
                "Loading, downloading catalog: {}".format(catalog_url))
            catalog = self._load_catalog_from_url_tsafe(catalog_url)
            if catalog is None and catalog_cache_dict is not None:
                self.logger.warning(
                    'failing over to loading the catalog from the cache')
                self._load_catalog_from_cache_tsafe(catalog_cache_dict)
        if self._installed_plugins_treeview_tracker:
            self._load_missing_plugins_tsafe()
        self._update_status_bar_tsafe('Loading completed')
        self._installed_plugins_treeview_tracker = None

    def _load_missing_plugins_tsafe(self):
        local_model_row = None
        for plugin in self._installed_plugins_treeview_tracker.keys():
            self.logger.warning(
                "plugin {} was not found in any loaded catalog or repo, moving to locally installed"
                .format(plugin))
            self.config['plugins.installed'][plugin] = None
            self._installed_plugins_treeview_tracker[plugin] = None
        for model_row in self._model:
            if _ModelNamedRow(*model_row).id == _LOCAL_REPOSITORY_ID:
                gui_utilities.glib_idle_add_wait(self._model.remove,
                                                 model_row.iter)
                break
        else:
            raise RuntimeError('failed to find the local plugin repository')
        self._load_catalog_local_tsafe()

    def _load_catalog_from_cache_tsafe(self, catalog_cache_dict):
        catalog = None
        try:
            catalog = Catalog(catalog_cache_dict['value'])
        except (KeyError, TypeError) as error:
            self.logger.warning(
                "{0} error when trying to add catalog dict to manager".format(
                    error.__class__.__name))
        else:
            self.catalog_plugins.add_catalog(
                catalog, catalog_url=catalog_cache_dict['url'], cache=False)
            self._add_catalog_to_tree_tsafe(catalog)
        return catalog

    def _load_catalog_from_url_tsafe(self, catalog_url):
        catalog = None
        try:
            catalog = Catalog.from_url(catalog_url)
        except requests.exceptions.ConnectionError:
            self.logger.warning(
                "connection error trying to download catalog url: {}".format(
                    catalog_url))
            self._show_dialog_error_tsafe(
                'Catalog Loading Error',
                'Failed to download catalog, check your internet connection.')
        except Exception:
            self.logger.warning('failed to add catalog by url: ' + catalog_url,
                                exc_info=True)
            self._show_dialog_error_tsafe('Catalog Loading Error',
                                          'Failed to add catalog')
        else:
            self.catalog_plugins.add_catalog(catalog,
                                             catalog_url=catalog_url,
                                             cache=True)
            self._add_catalog_to_tree_tsafe(catalog)
        return catalog

    def _load_catalog_local_tsafe(self):
        """
		Load the plugins which are available into the treeview to make them
		visible to the user.
		"""
        self.logger.debug('loading the local catalog')
        pm = self.application.plugin_manager
        self.__load_errors = {}
        pm.load_all(on_error=self._on_plugin_load_error_tsafe)
        node = _ModelNode(_LOCAL_REPOSITORY_ID, None, True,
                          _LOCAL_REPOSITORY_TITLE, None, None, False, False,
                          False, _ROW_TYPE_CATALOG)

        for name, plugin in pm.loaded_plugins.items():
            if self.config['plugins.installed'].get(name):
                continue
            self.config['plugins.installed'][name] = None
            node.children.append(
                _ModelNamedRow(
                    id=plugin.name,
                    installed=True,
                    enabled=plugin.name in pm.enabled_plugins,
                    title=plugin.title,
                    compatibility='Yes' if plugin.is_compatible else 'No',
                    version=plugin.version,
                    visible_enabled=True,
                    visible_installed=True,
                    sensitive_installed=False,
                    type=_ROW_TYPE_PLUGIN))
        for name in self.__load_errors.keys():
            node.children.append(
                _ModelNamedRow(id=name,
                               installed=True,
                               enabled=False,
                               title="{0} (Load Failed)".format(name),
                               compatibility='No',
                               version='Unknown',
                               visible_enabled=True,
                               visible_installed=True,
                               sensitive_installed=False,
                               type=_ROW_TYPE_PLUGIN))
        gui_utilities.glib_idle_add_wait(self.__store_add_node, node)

    #
    # Signal Handlers
    #
    def signal_eventbox_button_press(self, widget, event):
        if not (event.type == Gdk.EventType.BUTTON_PRESS
                and event.button == Gdk.BUTTON_PRIMARY):
            return
        if not self._last_plugin_selected:
            return
        named_plugin = _ModelNamedRow(*self._last_plugin_selected)
        plugin_id = named_plugin.id
        if plugin_id is None:
            return
        if plugin_id in self.application.plugin_manager:
            klass = self.application.plugin_manager[plugin_id]
            compatibility_details = list(klass.compatibility)
        else:
            repo_model, catalog_model = self._get_plugin_model_parents(
                self._last_plugin_selected)
            compatibility_details = list(
                self.catalog_plugins.compatibility(catalog_model.id,
                                                   repo_model.id,
                                                   named_plugin.id))

        popover = Gtk.Popover()
        popover.set_relative_to(
            self.gobjects['label_plugin_info_for_compatible'])
        grid = Gtk.Grid()
        popover.add(grid)
        grid.insert_column(0)
        grid.insert_column(0)
        grid.insert_column(0)
        grid.set_column_spacing(3)

        compatibility_details.insert(0, ('Type', 'Value', 'Met'))
        row = 0
        for row, req in enumerate(compatibility_details):
            grid.insert_row(row)
            label = Gtk.Label(req[0])
            label.set_property('halign', Gtk.Align.START)
            grid.attach(label, 0, row, 1, 1)
            label = Gtk.Label(req[1])
            label.set_property('halign', Gtk.Align.START)
            grid.attach(label, 1, row, 1, 1)
            label = Gtk.Label(('Yes' if req[2] else 'No') if row else req[2])
            label.set_property('halign', Gtk.Align.END)
            grid.attach(label, 2, row, 1, 1)
        if not row:
            popover.destroy()
            return
        popover.show_all()

    def signal_expander_activate(self, expander):
        paned = self.gobjects['paned_plugins']
        if expander.get_property('expanded'):  # collapsing
            paned.set_position(paned.get_allocation().height +
                               self._paned_offset)

    def signal_label_activate_link(self, _, uri):
        utilities.open_uri(uri)

    def signal_paned_button_press_event(self, paned, event):
        return not self.gobjects['expander_info'].get_property('expanded')

    def signal_popup_menu_activate_reload(self, _):
        self._reload()

    def signal_popup_menu_activate_reload_all(self, _):
        self._worker_thread_start(self._load_catalogs_tsafe, refresh=True)

    def signal_popup_menu_activate_show_documentation(self, _):
        named_row = self._selected_named_row
        if named_row is None or named_row.type != _ROW_TYPE_PLUGIN:
            return
        if not named_row.installed:
            gui_utilities.show_dialog_warning(
                'No Documentation', self.window,
                'This plugin has no documentation.')
            return
        try:
            PluginDocumentationWindow(self.application, named_row.id)
        except FileNotFoundError as error:
            self.logger.warning(error.strerror)
            gui_utilities.show_dialog_warning(
                'No Documentation', self.window,
                error.strerror.capitalize() + '.')

    def signal_popup_menu_activate_update(self, _):
        model_row = self._selected_model_row
        named_row = None if model_row is None else _ModelNamedRow(*model_row)
        if named_row is None:
            return
        if not (named_row.type == _ROW_TYPE_PLUGIN and named_row.installed
                and named_row.sensitive_installed):
            return
        if not self._plugin_uninstall(model_row):
            gui_utilities.show_dialog_error(
                'Update Failed', self.window,
                'Failed to uninstall the existing plugin data.')
            return
        self._plugin_install(model_row)

    def signal_renderer_toggled_enable(self, _, path):
        model_row = self._model[path]
        named_row = _ModelNamedRow(*model_row)
        if named_row.type != _ROW_TYPE_PLUGIN:
            return
        if named_row.id not in self.application.plugin_manager.loaded_plugins:
            return

        if named_row.id in self.__load_errors:
            gui_utilities.show_dialog_error(
                'Can Not Enable Plugin', self.window,
                'Can not enable a plugin which failed to load.')
            return
        if named_row.enabled:
            self._plugin_disable(model_row)
        else:
            self._plugin_enable(model_row)

    def signal_renderer_toggled_install(self, _, path):
        model_row = self._model[path]
        named_row = _ModelNamedRow(*model_row)
        if named_row.type == _ROW_TYPE_PLUGIN and named_row.installed:
            self._plugin_uninstall(model_row)
        else:
            self._plugin_install(model_row)
        if named_row.enabled:
            self._plugin_enable(model_row)

    def signal_treeview_row_activated(self, treeview, path, column):
        model_row = self._model[path]
        self._set_info(model_row)
        self._popup_menu_refresh(model_row)
class CampaignSelectionDialog(gui_utilities.GladeGObject):
    """
	Display a dialog which allows a new campaign to be created or an
	existing campaign to be opened.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('button_new_campaign', 'button_select',
                  'drawingarea_color_key', 'label_campaign_info',
                  'menubutton_filter', 'treeview_campaigns'),
        top_level=('StockAddImage', ))
    top_gobject = 'dialog'

    def __init__(self, *args, **kwargs):
        super(CampaignSelectionDialog, self).__init__(*args, **kwargs)
        treeview = self.gobjects['treeview_campaigns']
        self.treeview_manager = managers.TreeViewManager(
            treeview,
            cb_delete=self._prompt_to_delete_row,
            cb_refresh=self.load_campaigns)
        self.treeview_manager.set_column_titles(
            ('Campaign Name', 'Company', 'Type', 'Messages', 'Created By',
             'Creation Date', 'Expiration'),
            column_offset=1)
        self.treeview_manager.set_column_color(background=8, foreground=9)
        treeview.set_tooltip_column(10)
        self.popup_menu = self.treeview_manager.get_popup_menu()
        self._creation_assistant = None

        self._tv_model = Gtk.ListStore(str, str, str, str, str, str, str, str,
                                       Gdk.RGBA, Gdk.RGBA, str)
        # default sort is descending by campaign creation date
        self._tv_model.set_sort_column_id(6, Gtk.SortType.DESCENDING)

        # create and set the filter for expired campaigns
        self._tv_model_filter = self._tv_model.filter_new()
        self._tv_model_filter.set_visible_func(self._filter_campaigns)
        tv_model = Gtk.TreeModelSort(model=self._tv_model_filter)
        tv_model.set_sort_func(
            4, gui_utilities.gtk_treesortable_sort_func_numeric, 4)
        treeview.set_model(tv_model)

        # setup menus for filtering campaigns and load campaigns
        self.get_popup_filter_menu()
        self.load_campaigns()

    def get_popup_filter_menu(self):
        # create filter menu and menuitems
        filter_menu = Gtk.Menu()
        menu_item_expired = Gtk.CheckMenuItem('Expired campaigns')
        menu_item_user = Gtk.CheckMenuItem('Your campaigns')
        menu_item_other = Gtk.CheckMenuItem('Other campaigns')
        self.filter_menu_items = {
            'expired_campaigns': menu_item_expired,
            'your_campaigns': menu_item_user,
            'other_campaigns': menu_item_other
        }
        # set up the menuitems and add it to the menubutton
        for menus in self.filter_menu_items:
            filter_menu.append(self.filter_menu_items[menus])
            self.filter_menu_items[menus].connect(
                'toggled', self.signal_checkbutton_toggled)
            self.filter_menu_items[menus].show()
        self.filter_menu_items['expired_campaigns'].set_active(
            self.config['filter.campaign.expired'])
        self.filter_menu_items['your_campaigns'].set_active(
            self.config['filter.campaign.user'])
        self.filter_menu_items['other_campaigns'].set_active(
            self.config['filter.campaign.other_users'])
        self.gobjects['menubutton_filter'].set_popup(filter_menu)
        filter_menu.connect('destroy', self._save_filter)

    def _save_filter(self, _):
        self.config['filter.campaign.expired'] = self.filter_menu_items[
            'expired_campaigns'].get_active()
        self.config['filter.campaign.user'] = self.filter_menu_items[
            'your_campaigns'].get_active()
        self.config['filter.campaign.other_users'] = self.filter_menu_items[
            'other_campaigns'].get_active()

    def _filter_campaigns(self, model, tree_iter, _):
        expiration_ts = model[tree_iter][7]
        campaign_owner = model[tree_iter][5]
        username = self.config['server_username']
        if not self.filter_menu_items['your_campaigns'].get_active():
            if username == campaign_owner:
                return False
        if not self.filter_menu_items['other_campaigns'].get_active():
            if username != campaign_owner:
                return False
        if expiration_ts is None:
            return True
        if utilities.parse_datetime(expiration_ts) < datetime.datetime.now():
            if not self.filter_menu_items['expired_campaigns'].get_active():
                return False
        return True

    def _highlight_campaign(self, campaign_name):
        treeview = self.gobjects['treeview_campaigns']
        model = treeview.get_model()
        model_iter = gui_utilities.gtk_list_store_search(model,
                                                         campaign_name,
                                                         column=1)
        if model_iter:
            treeview.set_cursor(model.get_path(model_iter), None, False)
            return True
        return False

    def _prompt_to_delete_row(self, treeview, selection):
        (model, tree_iter) = selection.get_selected()
        if not tree_iter:
            return
        campaign_id = model.get_value(tree_iter, 0)
        if self.config.get('campaign_id') == campaign_id:
            gui_utilities.show_dialog_warning(
                'Can Not Delete Campaign', self.dialog,
                'Can not delete the current campaign.')
            return
        if not gui_utilities.show_dialog_yes_no(
                'Delete This Campaign?', self.dialog,
                'This action is irreversible, all campaign data will be lost.'
        ):
            return
        self.application.emit('campaign-delete', campaign_id)
        self.load_campaigns()
        self._highlight_campaign(self.config.get('campaign_name'))

    def load_campaigns(self):
        """Load campaigns from the remote server and populate the :py:class:`Gtk.TreeView`."""
        store = self._tv_model
        store.clear()
        style_context = self.dialog.get_style_context()
        bg_color = gui_utilities.gtk_style_context_get_color(
            style_context, 'theme_color_tv_bg', default=ColorHexCode.WHITE)
        fg_color = gui_utilities.gtk_style_context_get_color(
            style_context, 'theme_color_tv_fg', default=ColorHexCode.BLACK)
        hlbg_color = gui_utilities.gtk_style_context_get_color(
            style_context,
            'theme_color_tv_hlbg',
            default=ColorHexCode.LIGHT_YELLOW)
        hlfg_color = gui_utilities.gtk_style_context_get_color(
            style_context, 'theme_color_tv_hlfg', default=ColorHexCode.BLACK)
        now = datetime.datetime.now()
        campaigns_query = """\
		{
			db {
				campaigns {
					edges {
						node {
							id,
							name,
							description,
							company {
								name
							},
							campaignType {
								name
							},
							messages {
								total
							},
							user {
								id
							},
							created,
							expiration
						}
					}
				}
			}
		}
		"""
        campaigns = self.application.rpc.graphql(campaigns_query)
        for campaign in campaigns['db']['campaigns']['edges']:
            campaign = campaign['node']
            created_ts = utilities.datetime_utc_to_local(campaign['created'])
            created_ts = utilities.format_datetime(created_ts)
            expiration_ts = campaign['expiration']
            is_expired = False
            if expiration_ts is not None:
                expiration_ts = utilities.datetime_utc_to_local(expiration_ts)
                if expiration_ts < now:
                    is_expired = True
                expiration_ts = utilities.format_datetime(expiration_ts)
            store.append((str(campaign['id']), campaign['name'],
                          (campaign['company']['name']
                           if campaign['company'] is not None else None),
                          (campaign['campaignType']['name']
                           if campaign['campaignType'] is not None else None),
                          "{0:,}".format(campaign['messages']['total']),
                          campaign['user']['id'], created_ts, expiration_ts,
                          (hlbg_color if is_expired else bg_color),
                          (hlfg_color if is_expired else fg_color),
                          (html.escape(campaign['description'], quote=True)
                           if campaign['description'] else None)))
        self.gobjects['label_campaign_info'].set_text(
            "Showing {0} of {1:,} Campaign{2}".format(
                len(self._tv_model_filter), len(self._tv_model),
                ('' if len(self._tv_model) == 1 else 's')))

    def signal_assistant_destroy(self, _, campaign_creation_assistant):
        self._creation_assistant = None
        campaign_name = campaign_creation_assistant.campaign_name
        if not campaign_name:
            return
        self.load_campaigns()
        self._highlight_campaign(campaign_name)

    def signal_button_clicked(self, button):
        if self._creation_assistant is not None:
            gui_utilities.show_dialog_warning(
                'Campaign Creation Assistant', self.dialog,
                'The campaign creation assistant is already active.')
            return
        assistant = CampaignAssistant(self.application)
        assistant.assistant.set_transient_for(self.dialog)
        assistant.assistant.set_modal(True)
        assistant.assistant.connect('destroy', self.signal_assistant_destroy,
                                    assistant)
        assistant.interact()
        self._creation_assistant = assistant

    def signal_checkbutton_toggled(self, _):
        self._tv_model_filter.refilter()
        self.gobjects['label_campaign_info'].set_text(
            "Showing {0} of {1:,} Campaign{2}".format(
                len(self._tv_model_filter), len(self._tv_model),
                ('' if len(self._tv_model) == 1 else 's')))

    def signal_drawingarea_draw(self, drawingarea, context):
        width, height = drawingarea.get_size_request()
        context.rectangle(0, 0, width, height)
        context.stroke_preserve()
        style_context = self.dialog.get_style_context()
        hlbg_color = gui_utilities.gtk_style_context_get_color(
            style_context,
            'theme_color_tv_hlbg',
            default=ColorHexCode.LIGHT_YELLOW)
        context.set_source_rgb(hlbg_color.red, hlbg_color.green,
                               hlbg_color.blue)
        context.fill()

    def signal_treeview_row_activated(self, treeview, treeview_column,
                                      treepath):
        self.gobjects['button_select'].emit('clicked')

    def interact(self):
        self._highlight_campaign(self.config.get('campaign_name'))
        self.dialog.show_all()
        response = self.dialog.run()
        old_campaign_id = self.config.get('campaign_id')
        old_campaign_name = self.config.get('campaign_name')
        while response != Gtk.ResponseType.CANCEL:
            treeview = self.gobjects['treeview_campaigns']
            selection = treeview.get_selection()
            (model, tree_iter) = selection.get_selected()
            if tree_iter:
                break
            gui_utilities.show_dialog_error(
                'No Campaign Selected', self.dialog,
                'Either select a campaign or create a new one.')
            response = self.dialog.run()
        if response == Gtk.ResponseType.APPLY:
            campaign_id = model.get_value(tree_iter, 0)
            self.config['campaign_id'] = campaign_id
            campaign_name = model.get_value(tree_iter, 1)
            self.config['campaign_name'] = campaign_name
            if not (campaign_id == old_campaign_id
                    and campaign_name == old_campaign_name):
                self.application.emit('campaign-set', campaign_id)
        self.dialog.destroy()
        return response
Beispiel #28
0
class MainMenuBar(gui_utilities.GladeGObject):
    """
	The main menu bar for the primary application window. This configures any
	optional menu items as well as handles all the menu item signals
	appropriately.
	"""
    dependencies = gui_utilities.GladeDependencies(
        top_level=('StockDeleteImage', 'StockEditImage', 'StockHelpImage',
                   'StockPropertiesImage', 'StockStopImage'))
    top_gobject = 'menubar'

    def __init__(self, application, window):
        assert isinstance(window, MainAppWindow)
        super(MainMenuBar, self).__init__(application)
        self.window = weakref.proxy(window)
        self._add_accelerators()
        graphs_menu_item = self.gtk_builder_get('menuitem_tools_create_graph')
        if graphs.has_matplotlib:
            graphs_submenu = Gtk.Menu.new()
            for graph_name in graphs.get_graphs():
                graph = graphs.get_graph(graph_name)
                menu_item = Gtk.MenuItem.new_with_label(graph.name_human)
                menu_item.connect('activate',
                                  self.do_tools_show_campaign_graph,
                                  graph_name)
                graphs_submenu.append(menu_item)
            graphs_menu_item.set_submenu(graphs_submenu)
            graphs_menu_item.show_all()
        else:
            graphs_menu_item.set_sensitive(False)

    def _add_accelerators(self):
        accelerators = (('file_open', Gdk.KEY_o,
                         Gdk.ModifierType.CONTROL_MASK),
                        ('file_quit', Gdk.KEY_q,
                         Gdk.ModifierType.CONTROL_MASK),
                        ('tools_rpc_terminal', Gdk.KEY_F1,
                         Gdk.ModifierType.CONTROL_MASK),
                        ('tools_sftp_client', Gdk.KEY_F2,
                         Gdk.ModifierType.CONTROL_MASK))
        for menu_name, key, modifier in accelerators:
            menu_item = self.gtk_builder_get('menuitem_' + menu_name)
            menu_item.add_accelerator('activate', self.window.accel_group, key,
                                      modifier, Gtk.AccelFlags.VISIBLE)

    def do_edit_configure_campaign(self, _):
        self.application.campaign_configure()

    def do_edit_delete_campaign(self, _):
        self.application.campaign_delete()

    def do_edit_preferences(self, _):
        self.application.show_preferences()

    def do_edit_stop_service(self, _):
        self.application.stop_remote_service()

    def do_edit_companies(self, _):
        dialogs.CompanyEditorDialog(self.application).interact()

    def do_edit_tags(self, _):
        dialogs.TagEditorDialog(self.application).interact()

    def do_export_campaign_xlsx(self, _):
        self.window.export_campaign_xlsx()

    def do_export_campaign_xml(self, _):
        self.window.export_campaign_xml()

    def do_export_campaign_visit_geojson(self, _):
        self.window.export_campaign_visit_geojson()

    def do_export_message_data(self, _):
        self.window.export_message_data()

    def do_export_csv_credentials(self, _):
        campaign_tab = self.window.tabs['campaign']
        credentials_tab = campaign_tab.tabs['credentials']
        credentials_tab.export_table_to_csv()

    def do_export_csv_messages(self, _):
        campaign_tab = self.window.tabs['campaign']
        messages_tab = campaign_tab.tabs['messages']
        messages_tab.export_table_to_csv()

    def do_export_csv_visits(self, _):
        campaign_tab = self.window.tabs['campaign']
        visits_tab = campaign_tab.tabs['visits']
        visits_tab.export_table_to_csv()

    def do_import_message_data(self, _):
        self.window.import_message_data()

    def do_show_campaign_selection(self, _):
        self.application.show_campaign_selection()

    def do_quit(self, _):
        self.application.quit(optional=True)

    def do_tools_rpc_terminal(self, _):
        rpc_terminal.RPCTerminal(self.application)

    def do_tools_clone_page(self, _):
        dialogs.ClonePageDialog(self.application).interact()

    def do_tools_sftp_client(self, _):
        self.application.start_sftp_client()

    def do_tools_show_campaign_graph(self, _, graph_name):
        self.application.show_campaign_graph(graph_name)

    def do_help_about(self, _):
        dialogs.AboutDialog(self.application).interact()

    def do_help_templates(self, _):
        utilities.open_uri(
            'https://github.com/securestate/king-phisher-templates')

    def do_help_wiki(self, _):
        utilities.open_uri('https://github.com/securestate/king-phisher/wiki')
Beispiel #29
0
class ClonePageDialog(gui_utilities.GladeGObject):
    """
	Display a dialog for cloning a web page. The logic for the cloning operation
	is provided by the :py:mod:`.web_cloner` module.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('button_cancel', 'entry_clone_directory', 'label_status',
                  'spinner_status', 'treeview_resources'),
        top_level=('StockExecuteImage', 'StockStopImage'))
    view_columns = (
        extras.ColumnDefinitionString('Resource Path'),
        extras.ColumnDefinitionString('MIME Type'),
        extras.ColumnDefinitionBytes('Size'),
    )
    top_gobject = 'dialog'

    def __init__(self, *args, **kwargs):
        super(ClonePageDialog, self).__init__(*args, **kwargs)
        self.resources = Gtk.ListStore(*tuple(column.g_type
                                              for column in self.view_columns))
        treeview = self.gobjects['treeview_resources']
        treeview.set_model(self.resources)
        self.treeview_manager = managers.TreeViewManager(treeview)
        self.treeview_manager.set_column_titles(
            tuple(column.title for column in self.view_columns),
            renderers=tuple(column.cell_renderer()
                            for column in self.view_columns))
        self.popup_menu = self.treeview_manager.get_popup_menu()

        self.button_cancel = self.gobjects['button_cancel']
        self.entry_directory = self.gobjects['entry_clone_directory']
        # managed separately to be kept out of the config
        self.entry_target = self.gtk_builder_get('entry_target')
        self.label_status = self.gobjects['label_status']
        self.spinner_status = self.gobjects['spinner_status']

    def set_status(self, status_text, spinner_active=False):
        self.label_status.set_text("Status: {0}".format(status_text))
        self.spinner_status.set_property('visible', spinner_active)
        self.spinner_status.set_property('active', spinner_active)

    def interact(self):
        self.dialog.show_all()
        self.set_status('Waiting')
        if not web_cloner.has_webkit2:
            gui_utilities.show_dialog_error(
                'WebKit2GTK+ Is Unavailable', self.dialog,
                'The WebKit2GTK+ package is not available.')
            self.dialog.destroy()
            return
        while self.dialog.run() == Gtk.ResponseType.APPLY:
            target_url = self.entry_target.get_text()
            if not target_url:
                gui_utilities.show_dialog_error('Missing Information',
                                                self.dialog,
                                                'Please set the target URL.')
                self.set_status('Missing Information')
                continue
            dest_dir = self.entry_directory.get_text()
            if not dest_dir:
                gui_utilities.show_dialog_error(
                    'Missing Information', self.dialog,
                    'Please set the destination directory.')
                self.set_status('Missing Information')
                continue
            if not os.access(dest_dir, os.W_OK):
                gui_utilities.show_dialog_error(
                    'Invalid Directory', self.dialog,
                    'Can not write to the specified directory.')
                self.set_status('Invalid Directory')
                continue
            self.objects_save_to_config()

            self.set_status('Cloning', spinner_active=True)
            cloner = web_cloner.WebPageCloner(target_url, dest_dir)
            signal_id = self.button_cancel.connect(
                'clicked', lambda _: cloner.stop_cloning())
            original_label = self.button_cancel.get_label()
            self.button_cancel.set_label('Cancel')
            cloner.wait()
            self.button_cancel.set_label(original_label)
            self.button_cancel.disconnect(signal_id)

            if cloner.load_failed:
                self.set_status('Failed')
                gui_utilities.show_dialog_error(
                    'Operation Failed', self.dialog,
                    'The web page clone operation failed.')
                continue
            for resource in cloner.cloned_resources.values():
                if gui_utilities.gtk_list_store_search(self.resources,
                                                       resource.resource,
                                                       column=0):
                    continue
                self.resources.append(
                    _ModelNamedRow(path=resource.resource,
                                   mime_type=resource.mime_type or 'N/A',
                                   size=resource.size))
            self.set_status('Done')
            gui_utilities.gtk_sync()
        if len(self.resources) and gui_utilities.show_dialog_yes_no(
                'Transfer Cloned Pages', self.dialog,
                'Would you like to start the SFTP client\nto upload the cloned pages?'
        ):
            self.application.emit('sftp-client-start')
        self.dialog.destroy()

    def signal_multi_set_directory(self, _):
        dialog = extras.FileChooserDialog('Destination Directory', self.dialog)
        response = dialog.run_quick_select_directory()
        dialog.destroy()
        if response:
            self.entry_directory.set_text(response['target_path'])
class CampaignCompWindow(gui_utilities.GladeGObject):
    """
	The window which allows the user to select campaigns and compare the data
	using graphical representation.
	"""
    dependencies = gui_utilities.GladeDependencies(
        children=('treeview_campaigns', 'scrolledwindow_compare',
                  'scrolledwindow_select', 'stackswitcher', 'box_compare',
                  'box_select', 'stack_main'), )
    top_gobject = 'window'

    def __init__(self, *args, **kwargs):
        super(CampaignCompWindow, self).__init__(*args, **kwargs)
        self.comp_graph = CampaignCompGraph(
            self.application, style_context=self.application.style_context)
        self.gobjects['scrolledwindow_compare'].add(self.comp_graph.canvas)
        self.gobjects['box_compare'].pack_end(
            self.comp_graph.navigation_toolbar, False, False, 0)
        self.comp_graph.navigation_toolbar.hide()
        treeview = self.gobjects['treeview_campaigns']
        tvm = managers.TreeViewManager(treeview,
                                       cb_refresh=self.load_campaigns)
        toggle_renderer = Gtk.CellRendererToggle()
        toggle_renderer.connect('toggled', self.signal_renderer_toggled)
        self.stack = self.gobjects['stack_main']
        self.prev_child = self.stack.get_visible_child()
        b = Gtk.CellRendererText()
        tvm.set_column_titles(('Compare', 'Name', 'Company', 'Type',
                               'Created By', 'Creation Date', 'Expiration'),
                              column_offset=1,
                              renderers=(toggle_renderer, b, b, b, b, b, b))
        self._model = Gtk.ListStore(str, bool, str, str, str, str, str, str)
        self._model.set_sort_column_id(2, Gtk.SortType.ASCENDING)
        treeview.set_model(self._model)
        self.load_campaigns()
        self.window.show()

    def load_campaigns(self):
        """Load campaigns from the remote server and populate the :py:class:`Gtk.TreeView`."""
        store = self._model
        store.clear()
        for campaign in self.application.rpc.remote_table('campaigns'):
            company = campaign.company
            if company:
                company = company.name
            created_ts = utilities.datetime_utc_to_local(campaign.created)
            created_ts = utilities.format_datetime(created_ts)
            campaign_type = campaign.campaign_type
            if campaign_type:
                campaign_type = campaign_type.name
            expiration_ts = campaign.expiration
            if expiration_ts is not None:
                expiration_ts = utilities.datetime_utc_to_local(
                    campaign.expiration)
                expiration_ts = utilities.format_datetime(expiration_ts)
            store.append(
                (str(campaign.id), False, campaign.name, company,
                 campaign_type, campaign.user_id, created_ts, expiration_ts))

    def signal_renderer_toggled(self, _, path):
        campaign = self._model[path]  # pylint: disable=unsubscriptable-object
        campaign[1] = not campaign[1]

    def signal_stackswitcher_button_release(self, widget, event):
        view = self.stack.get_visible_child()
        if view == self.gobjects[
                'box_compare'] and self.prev_child == self.gobjects[
                    'box_select']:
            campaigns = [campaign for campaign in self._model if campaign[1]]  # pylint: disable=not-an-iterable
            campaigns = sorted(campaigns, key=lambda campaign: campaign[6])
            campaigns = [campaign[0] for campaign in campaigns]
            self.comp_graph.load_graph(campaigns)
        self.prev_child = view