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')
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)
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
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)
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)
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
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
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']))
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()
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()
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())
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)
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
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')
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()
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')
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))
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()
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')
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('"', '"'))) 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('"', '"'))) 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('"', '"'), 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
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')
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