diff options
Diffstat (limited to 'zenmap/zenmapGUI')
48 files changed, 12170 insertions, 0 deletions
diff --git a/zenmap/zenmapGUI/About.py b/zenmap/zenmapGUI/About.py new file mode 100644 index 0000000..aa51edd --- /dev/null +++ b/zenmap/zenmapGUI/About.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import webbrowser + +from zenmapGUI.higwidgets.higdialogs import HIGDialog +from zenmapGUI.higwidgets.higwindows import HIGWindow +from zenmapGUI.higwidgets.higboxes import HIGVBox, HIGHBox, \ + hig_box_space_holder +from zenmapGUI.higwidgets.higbuttons import HIGButton +from zenmapGUI.higwidgets.hignotebooks import HIGNotebook +from zenmapGUI.higwidgets.higscrollers import HIGScrolledWindow +from zenmapGUI.higwidgets.higtextviewers import HIGTextView + +from zenmapCore.Name import APP_DISPLAY_NAME, APP_WEB_SITE, APP_COPYRIGHT, \ + NMAP_DISPLAY_NAME, NMAP_WEB_SITE, UMIT_DISPLAY_NAME, UMIT_WEB_SITE +from zenmapCore.Version import VERSION +import zenmapCore.I18N # lgtm[py/unused-import] + + +# Prevent loading PyXML +import xml +xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] + +# For escaping text in marked-up labels. +from xml.sax.saxutils import escape + + +class _program_entry(Gtk.Box): + """A little box containing labels with a program's name and + description and a clickable link to its web site.""" + + # The amount of space to put between the name of a program and its + # web site button. + NAME_WEB_SITE_SPACING = 20 + + def __init__(self, name=None, web_site=None, description=None): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + + self.hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, + self.NAME_WEB_SITE_SPACING) + self.pack_start(self.hbox, True, True, 0) + + if name is not None: + name_label = Gtk.Label() + name_label.set_markup( + '<span size="large" weight="bold">%s</span>' % escape( + name)) + self.hbox.pack_start(name_label, False, True, 0) + + if web_site is not None: + web_site_button = Gtk.LinkButton.new(web_site) + web_site_button.connect("clicked", self._link_button_open) + self.hbox.pack_start(web_site_button, False, True, 0) + + if description is not None: + description_label = Gtk.Label() + description_label.set_alignment(0.0, 0.0) + description_label.set_line_wrap(True) + description_label.set_text(description) + self.pack_start(description_label, True, True, 0) + + def _link_button_open(self, widget): + webbrowser.open(widget.get_uri()) + + +class About(HIGDialog): + """An about dialog showing information about the program. It is meant to + have roughly the same feel as Gtk.AboutDialog.""" + def __init__(self): + HIGDialog.__init__(self) + self.set_title(_("About %s and %s") % ( + NMAP_DISPLAY_NAME, APP_DISPLAY_NAME)) + + self.vbox.set_border_width(12) + self.vbox.set_spacing(12) + + label = Gtk.Label() + label.set_markup( + '<span size="xx-large" weight="bold">%s %s</span>' % ( + escape(APP_DISPLAY_NAME), escape(VERSION))) + label.set_selectable(True) + self.vbox.pack_start(label, True, True, 0) + + label = Gtk.Label() + label.set_markup( + '<span size="small">%s</span>' % (escape(APP_COPYRIGHT))) + self.vbox.pack_start(label, True, True, 0) + + entry = _program_entry(NMAP_DISPLAY_NAME, NMAP_WEB_SITE, _( + "%s is a free and open source utility for network exploration " + "and security auditing.") % NMAP_DISPLAY_NAME) + self.vbox.pack_start(entry, True, True, 0) + + entry = _program_entry(APP_DISPLAY_NAME, APP_WEB_SITE, _( + "%s is a multi-platform graphical %s frontend and results viewer. " + "It was originally derived from %s.") % ( + APP_DISPLAY_NAME, NMAP_DISPLAY_NAME, UMIT_DISPLAY_NAME)) + self.vbox.pack_start(entry, True, True, 0) + + entry = _program_entry(UMIT_DISPLAY_NAME, UMIT_WEB_SITE, _( + "%s is an %s GUI created as part of the Nmap/Google Summer " + "of Code program.") % (UMIT_DISPLAY_NAME, NMAP_DISPLAY_NAME)) + button = Gtk.Button.new_with_label(_("%s credits") % UMIT_DISPLAY_NAME) + button.connect("clicked", self._show_umit_credits) + entry.hbox.pack_start(button, False, True, 0) + self.vbox.pack_start(entry, True, True, 0) + + self.vbox.show_all() + + close_button = self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CANCEL) + self.set_default_response(Gtk.ResponseType.CANCEL) + close_button.grab_focus() + + self.set_resizable(False) + + self._umit_credits_dialog = None + + self.connect("response", self._close) + + def _close(self, widget, response): + if self._umit_credits_dialog is not None: + self._umit_credits_dialog.destroy() + self._umit_credits_dialog = None + + self.hide() + + def _show_umit_credits(self, widget): + if self._umit_credits_dialog is not None: + self._umit_credits_dialog.present() + return + + self._umit_credits_dialog = UmitCredits() + + def credits_destroyed(widget): + # Mark that the credits dialog has been destroyed. + self._umit_credits_dialog = None + + self._umit_credits_dialog.connect("destroy", credits_destroyed) + self._umit_credits_dialog.show_all() + + +class UmitCredits(HIGWindow): + def __init__(self): + HIGWindow.__init__(self) + self.set_title(_("%s credits") % UMIT_DISPLAY_NAME) + self.set_size_request(-1, 250) + self.set_position(Gtk.WindowPosition.CENTER) + + self.__create_widgets() + self.__packing() + self.set_text() + + def __create_widgets(self): + self.vbox = HIGVBox() + self.hbox = HIGHBox() + self.notebook = HIGNotebook() + self.btn_close = HIGButton(stock=Gtk.STOCK_CLOSE) + + self.written_by_scroll = HIGScrolledWindow() + self.written_by_text = HIGTextView() + + self.design_scroll = HIGScrolledWindow() + self.design_text = HIGTextView() + + self.soc2007_scroll = HIGScrolledWindow() + self.soc2007_text = HIGTextView() + + self.contributors_scroll = HIGScrolledWindow() + self.contributors_text = HIGTextView() + + self.translation_scroll = HIGScrolledWindow() + self.translation_text = HIGTextView() + + self.nokia_scroll = HIGScrolledWindow() + self.nokia_text = HIGTextView() + + def __packing(self): + self.add(self.vbox) + self.vbox.set_spacing(12) + self.vbox._pack_expand_fill(self.notebook) + self.vbox._pack_noexpand_nofill(self.hbox) + + self.hbox._pack_expand_fill(hig_box_space_holder()) + self.hbox._pack_noexpand_nofill(self.btn_close) + + self.notebook.append_page( + self.written_by_scroll, Gtk.Label.new(_("Written by"))) + self.notebook.append_page( + self.design_scroll, Gtk.Label.new(_("Design"))) + self.notebook.append_page( + self.soc2007_scroll, Gtk.Label.new("SoC 2007")) + self.notebook.append_page( + self.contributors_scroll, Gtk.Label.new(_("Contributors"))) + self.notebook.append_page( + self.translation_scroll, Gtk.Label.new(_("Translation"))) + self.notebook.append_page( + self.nokia_scroll, Gtk.Label.new("Maemo")) + + self.written_by_scroll.add(self.written_by_text) + self.written_by_text.set_wrap_mode(Gtk.WrapMode.NONE) + + self.design_scroll.add(self.design_text) + self.design_text.set_wrap_mode(Gtk.WrapMode.NONE) + + self.soc2007_scroll.add(self.soc2007_text) + self.soc2007_text.set_wrap_mode(Gtk.WrapMode.NONE) + + self.contributors_scroll.add(self.contributors_text) + self.contributors_text.set_wrap_mode(Gtk.WrapMode.NONE) + + self.translation_scroll.add(self.translation_text) + self.translation_text.set_wrap_mode(Gtk.WrapMode.NONE) + + self.nokia_scroll.add(self.nokia_text) + self.nokia_text.set_wrap_mode(Gtk.WrapMode.NONE) + + self.btn_close.connect('clicked', lambda x, y=None: self.destroy()) + + def set_text(self): + b = self.written_by_text.get_buffer() + b.set_text("""Adriano Monteiro Marques <py.adriano@gmail.com>""") + + b = self.design_text.get_buffer() + b.set_text("""Operating System and Vulnerability Icons: +Takeshi Alexandre Gondo <sinistrofumanchu@yahoo.com.br> + +Logo, Application Icons and Splash screen: +Virgílio Carlo de Menezes Vasconcelos <virgiliovasconcelos@gmail.com> + +The Umit Project Web Site Design: +Joao Paulo Pacheco <jp.pacheco@gmail.com>""") + + b = self.soc2007_text.get_buffer() + b.set_text("""Independent Features: +Adriano Monteiro Marques <py.adriano@gmail.com> +Frederico Silva Ribeiro <fredegart@gmail.com> + +Network Inventory: +Guilherme Henrique Polo Gonçalves <ggpolo@gmail.com> + +Umit Radial Mapper: +João Paulo de Souza Medeiros <ignotus21@gmail.com> + +Profile/Wizard interface editor: +Luis Antonio Bastião Silva <luis.kop@gmail.com> + +NSE Facilitator: +Maxim I. Gavrilov <lovelymax@gmail.com> + +Umit Web: +Rodolfo da Silva Carvalho <rodolfo.ueg@gmail.com>""") + + b = self.contributors_text.get_buffer() + b.set_text("""Sponsored by (SoC 2005, 2006 and 2007): +Google <code.summer@gmail.com> + +Mentor of Umit for Google SoC 2005 and 2006: +Fyodor <fyodor@insecure.org> + +Mentor of Umit for Google SoC 2007 Projects: +Adriano Monteiro Marques <py.adriano@gmail.com> + +Initial development: +Adriano Monteiro Marques <py.adriano@gmail.com> +Cleber Rodrigues Rosa Junior <cleber.gnu@gmail.com> + +Nmap students from Google SoC 2007 that helped Umit: +Eddie Bell <ejlbell@gmail.com> +David Fifield <david@bamsoftware.com> +Kris Katterjohn <katterjohn@gmail.com> + +The Umit Project WebSite: +AbraoBarbosa dos Santos Neto <abraobsn@gmail.com> +Adriano Monteiro Marques <py.adriano@gmail.com> +Heitor de Lima Matos <heitordelima@hotmail.com> +Joao Paulo Pacheco <jp.pacheco@gmail.com> +João Paulo de Souza Medeiros <ignotus21@gmail.com> +Luis Antonio Bastião Silva <luis.kop@gmail.com> +Rodolfo da Silva Carvalho <rodolfo.ueg@gmail.com> + +Beta testers for 0.9.5RC1: +Drew Miller <securitygeek@fribble.org> +Igor Feghali <ifeghali@php.net> +Joao Paulo Pacheco <jp.pacheco@gmail.com> +Luis Antonio Bastião Silva <luis.kop@gmail.com> +<ray-solomon@excite.com> +<jah@zadkiel.plus.com> +<epatterson@directapps.com> + +Initial attempt on Maemo port: +Adriano Monteiro Marques <py.adriano@gmail.com> +Osvaldo Santana Neto <osantana@gmail.com>""") + + b = self.translation_text.get_buffer() + b.set_text("""Brazilian Portuguese: +Adriano Monteiro Marques <py.adriano@gmail.com>""") + + b = self.nokia_text.get_buffer() + b.set_text("""Adriano Monteiro Marques <py.adriano@gmail.com>""") + +if __name__ == '__main__': + about = About() + about.show() + about.connect("response", lambda widget, response: Gtk.main_quit()) + + Gtk.main() diff --git a/zenmap/zenmapGUI/App.py b/zenmap/zenmapGUI/App.py new file mode 100644 index 0000000..f417c75 --- /dev/null +++ b/zenmap/zenmapGUI/App.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import os +import signal +import sys +import configparser +import shutil + +# Cause an exception if PyGTK can't open a display. Normally this just +# produces a warning, but the lack of a display eventually causes a +# segmentation fault. See http://live.gnome.org/PyGTK/WhatsNew210. +# 'append = "True"' is to work around an error when using PyGTK with +# Python 2.7 that otherwise causes an assertion failure. See +# https://bugzilla.redhat.com/show_bug.cgi?id=620216#c10. +import warnings +warnings.filterwarnings("error", module="gtk", append="True") +try: + import gi + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk, Gdk +except Exception: + # On Mac OS X 10.5, X11 is supposed to be automatically launched on demand. + # It works by setting the DISPLAY environment variable to something like + # "/tmp/launch-XXXXXX/:0" and intercepting traffic on that socket; see + # http://homepage.mac.com/sao1/X11/#four. However this breaks in a strange + # way if DISPLAY is set in one of the shell startup files like .profile. + # Those files only have an effect on the shell, not the graphical + # environment, so X11 starts up as expected, but in doing so it reads the + # startup scripts, and for some reason the first connection (the one that + # caused the launch) is rejected. But somehow subsequent connections work + # fine! So if the import fails, try one more time. + import gi + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk, Gdk +warnings.resetwarnings() + +from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog + +from zenmapCore.UmitConf import is_maemo, SearchConfig +import zenmapCore.UmitConf +from zenmapCore.UmitLogging import log +from zenmapCore.UmitOptionParser import option_parser +from zenmapCore.Name import APP_NAME, APP_DISPLAY_NAME, NMAP_DISPLAY_NAME +import zenmapCore.I18N # lgtm[py/unused-import] +from zenmapCore.Paths import Path, create_user_config_dir +from zenmapCore.Name import APP_DISPLAY_NAME + +from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog + +# A global list of open scan windows. When the last one is destroyed, we call +# Gtk.main_quit. +open_windows = [] + + +def _destroy_callback(window): + open_windows.remove(window) + if len(open_windows) == 0: + Gtk.main_quit() + try: + from zenmapCore.UmitDB import UmitDB + except ImportError as e: + log.debug(">>> Not cleaning up database: %s." % str(e)) + else: + # Cleaning up data base + UmitDB().cleanup(SearchConfig().converted_save_time) + + +def new_window(): + from zenmapGUI.MainWindow import ScanWindow + w = ScanWindow() + w.connect("destroy", _destroy_callback) + if is_maemo(): + import hildon + hildon_app = hildon.Program() + hildon_app.add_window(w) + open_windows.append(w) + return w + + +def is_root(): + if 'NMAP_PRIVILEGED' in os.environ: + return True + elif 'NMAP_UNPRIVILEGED' in os.environ: + return False + else: + return sys.platform == "win32" or os.getuid() == 0 or is_maemo() + + +def install_excepthook(): + # This will catch exceptions and send them to bugzilla + def excepthook(type, value, tb): + import traceback + + traceback.print_exception(type, value, tb) + + # Cause an exception if PyGTK can't open a display. Normally this just + # produces a warning, but the lack of a display eventually causes a + # segmentation fault. See http://live.gnome.org/PyGTK/WhatsNew210. + warnings.filterwarnings("error", module="gtk") + import gi + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk, Gdk + warnings.resetwarnings() + + Gdk.threads_enter() + + from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog + from zenmapGUI.CrashReport import CrashReport + if type == ImportError: + d = HIGAlertDialog(type=Gtk.MessageType.ERROR, + message_format=_("Import error"), + secondary_text=_("""A required module was not found. + +""" + str(value))) + d.run() + d.destroy() + else: + c = CrashReport(type, value, tb) + c.show_all() + Gtk.main() + + Gdk.threads_leave() + + Gtk.main_quit() + + sys.excepthook = excepthook + + +def safe_shutdown(signum, stack): + """Kills any active scans/tabs and shuts down the application.""" + log.debug("\n\n%s\nSAFE SHUTDOWN!\n%s\n" % ("#" * 30, "#" * 30)) + log.debug("SIGNUM: %s" % signum) + + for window in open_windows: + window.scan_interface.kill_all_scans() + + sys.exit(signum) + + +def run(): + if os.name == "posix": + signal.signal(signal.SIGHUP, safe_shutdown) + signal.signal(signal.SIGTERM, safe_shutdown) + signal.signal(signal.SIGINT, safe_shutdown) + + DEVELOPMENT = os.environ.get(APP_NAME.upper() + "_DEVELOPMENT", False) + if not DEVELOPMENT: + install_excepthook() + + zenmapCore.I18N.install_gettext(Path.locale_dir) + + try: + # Create the ~/.zenmap directory by copying from the system-wide + # template directory. + create_user_config_dir( + Path.user_config_dir, Path.config_dir) + except (IOError, OSError) as e: + error_dialog = HIGAlertDialog( + message_format=_( + "Error creating the per-user configuration directory"), + secondary_text=_("""\ +There was an error creating the directory %s or one of the files in it. \ +The directory is created by copying the contents of %s. \ +The specific error was + +%s + +%s needs to create this directory to store information such as the list of \ +scan profiles. Check for access to the directory and try again.""") % ( + repr(Path.user_config_dir), repr(Path.config_dir), + repr(str(e)), APP_DISPLAY_NAME + ) + ) + error_dialog.run() + error_dialog.destroy() + sys.exit(1) + + try: + # Read the ~/.zenmap/zenmap.conf configuration file. + zenmapCore.UmitConf.config_parser.read(Path.user_config_file) + except configparser.ParsingError as e: + # ParsingError can leave some values as lists instead of strings. Just + # blow it all away if we have this problem. + zenmapCore.UmitConf.config_parser = zenmapCore.UmitConf.config_parser.__class__() + error_dialog = HIGAlertDialog( + message_format=_("Error parsing the configuration file"), + secondary_text=_("""\ +There was an error parsing the configuration file %s. \ +The specific error was + +%s + +%s can continue without this file but any information in it will be ignored \ +until it is repaired.""") % (Path.user_config_file, str(e), APP_DISPLAY_NAME) + ) + error_dialog.run() + error_dialog.destroy() + global_config_path = os.path.join(Path.config_dir, APP_NAME + '.conf') + repair_dialog = HIGAlertDialog( + type=Gtk.MessageType.QUESTION, + message_format=_("Restore default configuration?"), + secondary_text=_("""\ +To avoid further errors parsing the configuration file %s, \ +you can copy the default configuration from %s. + +Do this now? \ +""") % (Path.user_config_file, global_config_path), + ) + repair_dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) + repair_dialog.set_default_response(Gtk.ResponseType.CANCEL) + if repair_dialog.run() == Gtk.ResponseType.OK: + shutil.copyfile(global_config_path, Path.user_config_file) + log.debug(">>> Copy %s to %s." % (global_config_path, Path.user_config_file)) + repair_dialog.destroy() + + # Display a "you're not root" warning if appropriate. + if not is_root(): + non_root = NonRootWarning() + non_root.run() + non_root.destroy() + + # Load files given as command-line arguments. + filenames = option_parser.get_open_results() + if len(filenames) == 0: + # Open up a blank window. + window = new_window() + window.show_all() + else: + for filename in filenames: + window = new_window() + if os.path.isdir(filename): + window._load_directory(window.scan_interface, filename) + else: + window._load(window.scan_interface, filename) + window.show_all() + + nmap = option_parser.get_nmap() + target = option_parser.get_target() + profile = option_parser.get_profile() + + if nmap: + # Start running a scan if given by the -n option. + page = window.get_empty_interface() + page.command_toolbar.command = " ".join(nmap) + page.start_scan_cb() + elif target or profile: + # Set up target and profile according to the -t and -p options. + page = window.get_empty_interface() + if target: + page.toolbar.selected_target = target + if profile: + page.toolbar.selected_profile = profile + if target and profile: + page.start_scan_cb() + + Gtk.main() + + +class NonRootWarning (HIGAlertDialog): + def __init__(self): + warning_text = _('''You are trying to run %s with a non-root user! + +Some %s options need root privileges to work.''') % ( + APP_DISPLAY_NAME, NMAP_DISPLAY_NAME) + + HIGAlertDialog.__init__(self, message_format=_('Non-root user'), + secondary_text=warning_text) + +if __name__ == "__main__": + run() diff --git a/zenmap/zenmapGUI/BugReport.py b/zenmap/zenmapGUI/BugReport.py new file mode 100644 index 0000000..a74af6c --- /dev/null +++ b/zenmap/zenmapGUI/BugReport.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.higwidgets.higboxes import HIGVBox + +from zenmapCore.Name import APP_DISPLAY_NAME, NMAP_DISPLAY_NAME, NMAP_WEB_SITE +import zenmapCore.I18N # lgtm[py/unused-import] + + +# Prevent loading PyXML +import xml +xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] + +# For escaping text in marked-up labels. +from xml.sax.saxutils import escape + + +class BugReport(Gtk.Window, object): + def __init__(self): + Gtk.Window.__init__(self) + self.set_title(_('How to Report a Bug')) + self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + self.set_resizable(False) + + self._create_widgets() + self._pack_widgets() + self._connect_widgets() + + def _create_widgets(self): + self.vbox = HIGVBox() + self.button_box = Gtk.ButtonBox.new(Gtk.Orientation.HORIZONTAL) + + self.text = Gtk.Label() + + self.btn_ok = Gtk.Button.new_from_stock(Gtk.STOCK_OK) + + def _pack_widgets(self): + self.vbox.set_border_width(6) + + self.text.set_line_wrap(True) + self.text.set_max_width_chars(50) + self.text.set_markup(_("""\ +<big><b>How to report a bug</b></big> + +Like their author, %(nmap)s and %(app)s aren't perfect. But you can help \ +make it better by sending bug reports or even writing patches. If \ +%(nmap)s doesn't behave the way you expect, first upgrade to the latest \ +version available from <b>%(nmap_web)s</b>. If the problem persists, do \ +some research to determine whether it has already been discovered and \ +addressed. Try Googling the error message or browsing the nmap-dev \ +archives at http://seclists.org/. Read the full manual page as well. If \ +nothing comes of this, mail a bug report to \ +<b><dev@nmap.org></b>. Please include everything you have \ +learned about the problem, as well as what version of Nmap you are \ +running and what operating system version it is running on. Problem \ +reports and %(nmap)s usage questions sent to dev@nmap.org are \ +far more likely to be answered than those sent to Fyodor directly. + +Code patches to fix bugs are even better than bug reports. Basic \ +instructions for creating patch files with your changes are available at \ +https://nmap.org/data/HACKING. Patches may be sent to nmap-dev \ +(recommended) or to Fyodor directly. +""") % { + "app": escape(APP_DISPLAY_NAME), + "nmap": escape(NMAP_DISPLAY_NAME), + "nmap_web": escape(NMAP_WEB_SITE) + }) + self.vbox.add(self.text) + + self.button_box.set_layout(Gtk.ButtonBoxStyle.END) + self.button_box.pack_start(self.btn_ok, True, True, 0) + + self.vbox._pack_noexpand_nofill(self.button_box) + self.add(self.vbox) + + def _connect_widgets(self): + self.btn_ok.connect("clicked", self.close) + self.connect("delete-event", self.close) + + def close(self, widget=None, event=None): + self.destroy() + +if __name__ == "__main__": + w = BugReport() + w.show_all() + w.connect("delete-event", lambda x, y: Gtk.main_quit()) + + Gtk.main() diff --git a/zenmap/zenmapGUI/CrashReport.py b/zenmap/zenmapGUI/CrashReport.py new file mode 100644 index 0000000..337eab2 --- /dev/null +++ b/zenmap/zenmapGUI/CrashReport.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk + +import sys +import traceback + +from zenmapGUI.higwidgets.higdialogs import HIGDialog +from zenmapGUI.higwidgets.higboxes import HIGHBox + +from zenmapCore.Name import APP_DISPLAY_NAME +from zenmapCore.Version import VERSION +import zenmapCore.I18N # lgtm[py/unused-import] + + +# Prevent loading PyXML +import xml +xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] + +# For escaping text in marked-up labels. +from xml.sax.saxutils import escape + + +class CrashReport(HIGDialog): + def __init__(self, type, value, tb): + HIGDialog.__init__(self) + Gtk.Window.__init__(self) + self.set_title(_('Crash Report')) + self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + + self._create_widgets() + self._pack_widgets() + self._connect_widgets() + + trace = "".join(traceback.format_exception(type, value, tb)) + text = "Version: " + VERSION + "\n" + trace + self.description_text.get_buffer().set_text(text) + + def _create_widgets(self): + self.button_box = Gtk.ButtonBox.new(Gtk.Orientation.HORIZONTAL) + self.button_box_ok = Gtk.ButtonBox.new(Gtk.Orientation.HORIZONTAL) + + self.description_scrolled = Gtk.ScrolledWindow() + self.description_text = Gtk.TextView() + self.description_text.set_editable(False) + + self.bug_text = Gtk.Label() + self.bug_text.set_markup(_('An unexpected error has crashed ' + '%(app_name)s. Please copy the stack trace below and send it to ' + 'the <a href="mailto:dev@nmap.org">dev@nmap.org</a> mailing list. ' + '(<a href="http://seclists.org/nmap-dev/">More about the list.</a>' + ') The developers will see your report and try to fix the problem.' + ) % {"app_name": escape(APP_DISPLAY_NAME)}) + self.email_frame = Gtk.Frame() + self.email_label = Gtk.Label() + self.email_label.set_markup(_('<b>Copy and email to ' + '<a href="mailto:dev@nmap.org">dev@nmap.org</a>:</b>')) + self.btn_copy = Gtk.Button.new_from_stock(Gtk.STOCK_COPY) + self.btn_ok = Gtk.Button.new_from_stock(Gtk.STOCK_OK) + + self.hbox = HIGHBox() + + def _pack_widgets(self): + self.description_scrolled.add(self.description_text) + self.description_scrolled.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.description_scrolled.set_size_request(400, 150) + self.description_text.set_wrap_mode(Gtk.WrapMode.WORD) + + self.bug_text.set_max_width_chars(60) + self.bug_text.set_line_wrap(True) + self.email_label.set_line_wrap(True) + + self.email_frame.set_label_widget(self.email_label) + self.email_frame.set_shadow_type(Gtk.ShadowType.NONE) + + self.hbox.set_border_width(6) + self.vbox.set_border_width(6) + + self.hbox._pack_expand_fill(self.bug_text) + + self.button_box.set_layout(Gtk.ButtonBoxStyle.START) + self.button_box_ok.set_layout(Gtk.ButtonBoxStyle.END) + + self.button_box.pack_start(self.btn_copy, True, True, 0) + self.button_box_ok.pack_start(self.btn_ok, True, True, 0) + + self.vbox.pack_start(self.hbox, True, True, 0) + self.vbox.pack_start(self.email_frame, True, True, 0) + self.vbox.pack_start(self.description_scrolled, True, True, 0) + self.vbox.pack_start(self.button_box, True, True, 0) + self.action_area.pack_start(self.button_box_ok, True, True, 0) + + def _connect_widgets(self): + self.btn_ok.connect("clicked", self.close) + self.btn_copy.connect("clicked", self.copy) + self.connect("delete-event", self.close) + + def get_description(self): + buff = self.description_text.get_buffer() + return buff.get_text(buff.get_start_iter(), buff.get_end_iter(), include_hidden_chars=True) + + def copy(self, widget=None, event=None): + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clipboard.set_text(self.get_description(), -1) + clipboard.store() + + def close(self, widget=None, event=None): + self.destroy() + Gtk.main_quit() + sys.exit(0) + +if __name__ == "__main__": + c = CrashReport(None, None, None) + c.show_all() + c.connect("delete-event", lambda x, y: Gtk.main_quit()) + + Gtk.main() diff --git a/zenmap/zenmapGUI/DiffCompare.py b/zenmap/zenmapGUI/DiffCompare.py new file mode 100644 index 0000000..de946df --- /dev/null +++ b/zenmap/zenmapGUI/DiffCompare.py @@ -0,0 +1,456 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GObject, GLib + +import os +import os.path +import sys + +# Prevent loading PyXML +import xml +xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] + +import xml.sax + +from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog +from zenmapGUI.higwidgets.higboxes import HIGVBox, HIGHBox, \ + hig_box_space_holder +from zenmapGUI.higwidgets.higlabels import HIGSectionLabel +from zenmapGUI.higwidgets.higtables import HIGTable +from zenmapGUI.higwidgets.higbuttons import HIGButton + +from zenmapCore.NmapParser import NmapParser +from zenmapCore.UmitLogging import log +import zenmapCore.I18N # lgtm[py/unused-import] +import zenmapCore.Diff + +from zenmapGUI.FileChoosers import ResultsFileSingleChooserDialog + +# In milliseconds. +NDIFF_CHECK_TIMEOUT = 200 + + +class ScanChooser(HIGVBox): + """This class allows the selection of scan results from the list of open + tabs or from a file. It emits the "changed" signal when the scan selection + has changed.""" + + __gsignals__ = { + "changed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()) + } + + def __init__(self, scans, title): + HIGVBox.__init__(self) + + self.title = title + self.scan_dict = {} + + # Setting HIGVBox + self.set_border_width(5) + self.set_spacing(6) + + self._create_widgets() + self._pack_hbox() + self._attaching_widgets() + self._set_scrolled() + self._set_text_view() + self._set_open_button() + + for scan in scans: + self.add_scan(scan.scan_name or scan.get_nmap_command(), scan) + + self.combo_scan.connect('changed', self.show_scan) + self.combo_scan.connect('changed', lambda x: self.emit('changed')) + + self._pack_noexpand_nofill(self.lbl_scan) + self._pack_expand_fill(self.hbox) + + def _create_widgets(self): + self.lbl_scan = HIGSectionLabel(self.title) + self.hbox = HIGHBox() + self.table = HIGTable() + self.combo_scan = Gtk.ComboBoxText.new_with_entry() + self.btn_open_scan = Gtk.Button.new_from_stock(Gtk.STOCK_OPEN) + self.exp_scan = Gtk.Expander.new(_("Scan Output")) + self.scrolled = Gtk.ScrolledWindow() + self.txt_scan_result = Gtk.TextView() + self.txg_tag = Gtk.TextTag.new("scan_style") + + def get_buffer(self): + return self.txt_scan_result.get_buffer() + + def show_scan(self, widget): + nmap_output = self.get_nmap_output() + if nmap_output: + self.txt_scan_result.get_buffer().set_text(nmap_output) + + def normalize_output(self, output): + return "\n".join(output.split("\\n")) + + def _pack_hbox(self): + self.hbox._pack_noexpand_nofill(hig_box_space_holder()) + self.hbox._pack_expand_fill(self.table) + + def _attaching_widgets(self): + self.table.attach(self.combo_scan, 0, 1, 0, 1, yoptions=0) + self.table.attach( + self.btn_open_scan, 1, 2, 0, 1, yoptions=0, xoptions=0) + self.table.attach(self.exp_scan, 0, 2, 1, 2) + + def _set_scrolled(self): + self.scrolled.set_border_width(5) + self.scrolled.set_size_request(-1, 130) + + # Packing scrolled window into expander + self.exp_scan.add(self.scrolled) + + # Packing text view into scrolled window + self.scrolled.add_with_viewport(self.txt_scan_result) + + # Setting scrolled window + self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + def _set_text_view(self): + self.txg_table = self.txt_scan_result.get_buffer().get_tag_table() + self.txg_table.add(self.txg_tag) + self.txg_tag.set_property("family", "Monospace") + + self.txt_scan_result.set_wrap_mode(Gtk.WrapMode.WORD) + self.txt_scan_result.set_editable(False) + self.txt_scan_result.get_buffer().connect( + "changed", self._text_changed_cb) + + def _set_open_button(self): + self.btn_open_scan.connect('clicked', self.open_file) + + def open_file(self, widget): + file_chooser = ResultsFileSingleChooserDialog(_("Select Scan Result")) + + response = file_chooser.run() + file_chosen = file_chooser.get_filename() + file_chooser.destroy() + if response == Gtk.ResponseType.OK: + try: + parser = NmapParser() + parser.parse_file(file_chosen) + except xml.sax.SAXParseException as e: + alert = HIGAlertDialog( + message_format='<b>%s</b>' % _('Error parsing file'), + secondary_text=_( + "The file is not an Nmap XML output file. " + "The parsing error that occurred was\n%s") % str(e)) + alert.run() + alert.destroy() + return False + except Exception as e: + alert = HIGAlertDialog( + message_format='<b>%s</b>' % _( + 'Cannot open selected file'), + secondary_text=_("""\ + This error occurred while trying to open the file: + %s""") % str(e)) + alert.run() + alert.destroy() + return False + + scan_name = os.path.split(file_chosen)[-1] + self.add_scan(scan_name, parser) + + self.combo_scan.set_active(len(self.combo_scan.get_model()) - 1) + + def add_scan(self, scan_name, parser): + scan_id = 1 + new_scan_name = scan_name + while new_scan_name in self.scan_dict.keys(): + new_scan_name = "%s (%s)" % (scan_name, scan_id) + scan_id += 1 + + self.combo_scan.append_text(new_scan_name) + self.scan_dict[new_scan_name] = parser + + def _text_changed_cb(self, widget): + buff = self.txt_scan_result.get_buffer() + buff.apply_tag( + self.txg_tag, buff.get_start_iter(), buff.get_end_iter()) + + def get_parsed_scan(self): + """Return the currently selected scan's parsed output as an NmapParser + object, or None if no valid scan is selected.""" + selected_scan = self.combo_scan.get_active_text() + return self.scan_dict.get(selected_scan) + + def get_nmap_output(self): + """Return the currently selected scan's output as a string, or None if + no valid scan is selected.""" + if self.parsed_scan is not None: + return self.parsed_scan.get_nmap_output() + else: + return None + + nmap_output = property(get_nmap_output) + parsed_scan = property(get_parsed_scan) + + +class DiffWindow(Gtk.Window): + def __init__(self, scans): + Gtk.Window.__init__(self) + self.set_title(_("Compare Results")) + self.ndiff_process = None + # We allow the user to start a new diff before the old one has + # finished. We have to keep references to old processes until they + # finish to avoid problems when tearing down the Python interpreter at + # program exit. + self.old_processes = [] + self.timer_id = None + + self.main_vbox = HIGVBox() + self.diff_view = DiffView() + self.diff_view.set_size_request(-1, 100) + self.hbox_buttons = HIGHBox() + self.progress = Gtk.ProgressBar() + self.btn_close = HIGButton(stock=Gtk.STOCK_CLOSE) + self.hbox_selection = HIGHBox() + self.scan_chooser_a = ScanChooser(scans, _("A Scan")) + self.scan_chooser_b = ScanChooser(scans, _("B Scan")) + + self._pack_widgets() + self._connect_widgets() + + self.set_default_size(-1, 500) + + # Initial Size Request + self.initial_size = self.get_size() + + def _pack_widgets(self): + self.main_vbox.set_border_width(6) + + self.hbox_selection.pack_start(self.scan_chooser_a, True, True, 0) + self.hbox_selection.pack_start(self.scan_chooser_b, True, True, 0) + + self.main_vbox.pack_start(self.hbox_selection, False, True, 0) + + scroll = Gtk.ScrolledWindow() + scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroll.add(self.diff_view) + self.main_vbox.pack_start(scroll, True, True, 0) + + self.progress.hide() + self.progress.set_no_show_all(True) + self.hbox_buttons.pack_start(self.progress, False, True, 0) + self.hbox_buttons.pack_end(self.btn_close, False, True, 0) + + self.main_vbox._pack_noexpand_nofill(self.hbox_buttons) + + self.add(self.main_vbox) + + def _connect_widgets(self): + self.connect("delete-event", self.close) + self.btn_close.connect("clicked", self.close) + self.scan_chooser_a.connect('changed', self.refresh_diff) + self.scan_chooser_b.connect('changed', self.refresh_diff) + + def refresh_diff(self, widget): + """This method is called whenever the diff output might have changed, + such as when a different scan was selected in one of the choosers.""" + log.debug("Refresh diff.") + + if (self.ndiff_process is not None and + self.ndiff_process.poll() is None): + # Put this in the list of old processes we keep track of. + self.old_processes.append(self.ndiff_process) + self.ndiff_process = None + + scan_a = self.scan_chooser_a.parsed_scan + scan_b = self.scan_chooser_b.parsed_scan + + if scan_a is None or scan_b is None: + self.diff_view.clear() + else: + try: + self.ndiff_process = zenmapCore.Diff.ndiff(scan_a, scan_b) + except OSError as e: + alert = HIGAlertDialog( + message_format=_("Error running ndiff"), + secondary_text=_( + "There was an error running the ndiff program.\n\n" + ) + str(e)) + alert.run() + alert.destroy() + else: + self.progress.show() + if self.timer_id is None: + self.timer_id = GLib.timeout_add( + NDIFF_CHECK_TIMEOUT, self.check_ndiff_process) + + def check_ndiff_process(self): + """Check if the ndiff subprocess is done and show the diff if it is. + Also remove any finished processes from the old process list.""" + # Check if any old background processes have finished. + for p in self.old_processes[:]: + if p.poll() is not None: + p.close() + self.old_processes.remove(p) + + if self.ndiff_process is not None: + # We're running the most recent scan. Check if it's done. + status = self.ndiff_process.poll() + + if status is None: + # Keep calling this function on a timer until the process + # finishes. + self.progress.pulse() + return True + + if status == 0 or status == 1: + # Successful completion. + try: + diff = self.ndiff_process.get_scan_diff() + except zenmapCore.Diff.NdiffParseException as e: + alert = HIGAlertDialog( + message_format=_("Error parsing ndiff output"), + secondary_text=str(e)) + alert.run() + alert.destroy() + else: + self.diff_view.show_diff(diff) + else: + # Unsuccessful completion. + error_text = _( + "The ndiff process terminated with status code %d." + ) % status + stderr = self.ndiff_process.stderr.read() + if len(stderr) > 0: + error_text += "\n\n" + stderr + alert = HIGAlertDialog( + message_format=_("Error running ndiff"), + secondary_text=error_text) + alert.run() + alert.destroy() + + self.progress.hide() + self.ndiff_process.close() + self.ndiff_process = None + + if len(self.old_processes) > 0: + # Keep calling this callback. + return True + else: + # All done. + self.timer_id = None + return False + + def close(self, widget=None, extra=None): + self.destroy() + + +class DiffView(Gtk.TextView): + REMOVE_COLOR = "#ffaaaa" + ADD_COLOR = "#ccffcc" + + """A widget displaying a zenmapCore.Diff.ScanDiff.""" + def __init__(self): + Gtk.TextView.__init__(self) + self.set_editable(False) + + buff = self.get_buffer() + # Create text markup tags. + buff.create_tag("=", font="Monospace") + buff.create_tag( + "-", font="Monospace", background=self.REMOVE_COLOR) + buff.create_tag("+", font="Monospace", background=self.ADD_COLOR) + + def clear(self): + self.get_buffer().set_text("") + + def show_diff(self, diff): + self.clear() + buff = self.get_buffer() + for line in diff.splitlines(True): + if line.startswith("-"): + tags = ["-"] + elif line.startswith("+"): + tags = ["+"] + else: + tags = ["="] + buff.insert_with_tags_by_name(buff.get_end_iter(), line, *tags) + +if __name__ == "__main__": + from zenmapCore.NmapParser import NmapParser + + parsed1 = NmapParser() + parsed2 = NmapParser() + parsed3 = NmapParser() + parsed4 = NmapParser() + + parsed1.parse_file("test/xml_test1.xml") + parsed2.parse_file("test/xml_test2.xml") + parsed3.parse_file("test/xml_test3.xml") + parsed4.parse_file("test/xml_test4.xml") + + dw = DiffWindow({"Parsed 1": parsed1, + "Parsed 2": parsed2, + "Parsed 3": parsed3, + "Parsed 4": parsed4}) + + dw.show_all() + dw.connect("delete-event", lambda x, y: Gtk.main_quit()) + + Gtk.main() diff --git a/zenmap/zenmapGUI/FileChoosers.py b/zenmap/zenmapGUI/FileChoosers.py new file mode 100644 index 0000000..38b6a92 --- /dev/null +++ b/zenmap/zenmapGUI/FileChoosers.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import os.path +import sys + +import zenmapCore.I18N # lgtm[py/unused-import] + +RESPONSE_OPEN_DIRECTORY = 1 + + +class AllFilesFileFilter(Gtk.FileFilter): + def __init__(self): + Gtk.FileFilter.__init__(self) + + pattern = "*" + self.add_pattern(pattern) + self.set_name(_("All files (%s)") % pattern) + + +class ResultsFileFilter(Gtk.FileFilter): + def __init__(self): + Gtk.FileFilter.__init__(self) + + patterns = ["*.xml"] + for pattern in patterns: + self.add_pattern(pattern) + self.set_name(_("Nmap XML files (%s)") % ", ".join(patterns)) + + +class ScriptFileFilter(Gtk.FileFilter): + def __init__(self): + Gtk.FileFilter.__init__(self) + + patterns = ["*.nse"] + for pattern in patterns: + self.add_pattern(pattern) + self.set_name(_("NSE scripts (%s)") % ", ".join(patterns)) + + +class AllFilesFileChooserDialog(Gtk.FileChooserDialog): + def __init__(self, title="", parent=None, + action=Gtk.FileChooserAction.OPEN, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, Gtk.ResponseType.OK), backend=None): + + Gtk.FileChooserDialog.__init__(self, title=title, parent=parent, + action=action, buttons=buttons) + self.set_default_response(Gtk.ResponseType.OK) + self.add_filter(AllFilesFileFilter()) + + +class ResultsFileSingleChooserDialog(Gtk.FileChooserDialog): + """This results file choose only allows the selection of single files, not + directories.""" + def __init__(self, title="", parent=None, + action=Gtk.FileChooserAction.OPEN, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, Gtk.ResponseType.OK), backend=None): + + Gtk.FileChooserDialog.__init__(self, title=title, parent=parent, + action=action, buttons=buttons) + self.set_default_response(Gtk.ResponseType.OK) + for f in (ResultsFileFilter(), AllFilesFileFilter()): + self.add_filter(f) + + +class ResultsFileChooserDialog(Gtk.FileChooserDialog): + def __init__(self, title="", parent=None, + action=Gtk.FileChooserAction.OPEN, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + "Open Directory", RESPONSE_OPEN_DIRECTORY, + Gtk.STOCK_OPEN, Gtk.ResponseType.OK), backend=None): + + Gtk.FileChooserDialog.__init__(self, title=title, parent=parent, + action=action, buttons=buttons) + self.set_default_response(Gtk.ResponseType.OK) + for f in (ResultsFileFilter(), AllFilesFileFilter()): + self.add_filter(f) + + +class ScriptFileChooserDialog(Gtk.FileChooserDialog): + def __init__(self, title="", parent=None, + action=Gtk.FileChooserAction.OPEN, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, Gtk.ResponseType.OK), backend=None): + + Gtk.FileChooserDialog.__init__(self, title=title, parent=parent, + action=action, buttons=buttons) + self.set_default_response(Gtk.ResponseType.OK) + self.set_select_multiple(True) + for f in (ScriptFileFilter(), AllFilesFileFilter()): + self.add_filter(f) + + +class SaveResultsFileChooserDialog(Gtk.FileChooserDialog): + TYPES = ( + (_("By extension"), None, None), + (_("Nmap XML format (.xml)"), "xml", ".xml"), + (_("Nmap text format (.nmap)"), "text", ".nmap"), + ) + # For the "By Extension" choice. + EXTENSIONS = { + ".xml": "xml", + ".nmap": "text", + ".txt": "text", + } + + def __init__(self, title="", parent=None, + action=Gtk.FileChooserAction.SAVE, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK), backend=None): + + Gtk.FileChooserDialog.__init__(self, title=title, parent=parent, + action=action, buttons=buttons) + + types_store = Gtk.ListStore.new([str, str, str]) + for type in self.TYPES: + types_store.append(type) + + self.combo = Gtk.ComboBox.new_with_model(types_store) + cell = Gtk.CellRendererText() + self.combo.pack_start(cell, True) + self.combo.add_attribute(cell, "text", 0) + self.combo.connect("changed", self.combo_changed_cb) + self.combo.set_active(1) + + hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 6) + hbox.pack_end(self.combo, False, True, 0) + hbox.pack_end(Gtk.Label.new(_("Select File Type:")), False, True, 0) + hbox.show_all() + + self.set_extra_widget(hbox) + self.set_do_overwrite_confirmation(True) + + self.set_default_response(Gtk.ResponseType.OK) + + def combo_changed_cb(self, combo): + filename = self.get_filename() or "" + dir, basename = os.path.split(filename) + if dir != self.get_current_folder(): + self.set_current_folder(dir) + + # Find the recommended extension. + new_ext = combo.get_model().get_value(combo.get_active_iter(), 2) + if new_ext is not None: + # Change the filename to use the recommended extension. + root, ext = os.path.splitext(basename) + if len(ext) == 0 and root.startswith("."): + root = "" + self.set_current_name(root + new_ext) + + def get_extension(self): + return os.path.splitext(self.get_filename())[1] + + def get_format(self): + """Get the save format the user has chosen. It is a string, either + "text" or "xml".""" + filetype = self.combo.get_model().get_value( + self.combo.get_active_iter(), 1) + if filetype is None: + # Guess based on extension. "xml" is the default if unknown. + return self.EXTENSIONS.get(self.get_extension(), "xml") + return filetype + + +class DirectoryChooserDialog(Gtk.FileChooserDialog): + def __init__(self, title="", parent=None, + action=Gtk.FileChooserAction.SELECT_FOLDER, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, Gtk.ResponseType.OK), backend=None): + + Gtk.FileChooserDialog.__init__(self, title=title, parent=parent, + action=action, buttons=buttons) + self.set_default_response(Gtk.ResponseType.OK) + + +class SaveToDirectoryChooserDialog(Gtk.FileChooserDialog): + def __init__(self, title="", parent=None, + action=Gtk.FileChooserAction.SELECT_FOLDER, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK), backend=None): + + Gtk.FileChooserDialog.__init__(self, title=title, parent=parent, + action=action, buttons=buttons) + self.set_default_response(Gtk.ResponseType.OK) diff --git a/zenmap/zenmapGUI/FilterBar.py b/zenmap/zenmapGUI/FilterBar.py new file mode 100644 index 0000000..6b8f7ee --- /dev/null +++ b/zenmap/zenmapGUI/FilterBar.py @@ -0,0 +1,77 @@ +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GObject + +from zenmapGUI.higwidgets.higboxes import HIGHBox +from zenmapGUI.higwidgets.higlabels import HintWindow + + +class FilterBar(HIGHBox): + """This is the bar that appears while the host filter is active. It allows + entering a string that restricts the set of visible hosts.""" + + __gsignals__ = { + "changed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()) + } + + def __init__(self): + HIGHBox.__init__(self) + self.information_label = Gtk.Label() + self.entry = Gtk.Entry() + + self.pack_start(self.information_label, False, True, 0) + self.information_label.show() + + label = Gtk.Label.new(_("Host Filter:")) + self.pack_start(label, False, True, 0) + label.show() + + self.pack_start(self.entry, True, True, 0) + self.entry.show() + + help_button = Gtk.Button() + icon = Gtk.Image() + icon.set_from_stock(Gtk.STOCK_INFO, Gtk.IconSize.BUTTON) + help_button.add(icon) + help_button.connect("clicked", self._help_button_clicked) + self.pack_start(help_button, False, True, 0) + help_button.show_all() + + self.entry.connect("changed", lambda x: self.emit("changed")) + + def grab_focus(self): + self.entry.grab_focus() + + def get_filter_string(self): + return self.entry.get_text() + + def set_filter_string(self, filter_string): + return self.entry.set_text(filter_string) + + def set_information_text(self, text): + self.information_label.set_text(text) + + def _help_button_clicked(self, button): + hint_window = HintWindow(HELP_TEXT) + hint_window.show_all() + +HELP_TEXT = _("""\ +Entering the text into the search performs a <b>keyword search</b> - the \ +search string is matched against every aspect of the host. + +To refine the search, you can use <b>operators</b> to search only \ +specific fields within a host. Most operators have a short form, listed. \ + +<b>target: (t:)</b> - User-supplied target, or a rDNS result. +<b>os:</b> - All OS-related fields. +<b>open: (op:)</b> - Open ports discovered in a scan. +<b>closed: (cp:)</b> - Closed ports discovered in a scan. +<b>filtered: (fp:)</b> - Filtered ports discovered in scan. +<b>unfiltered: (ufp:)</b> - Unfiltered ports found in a scan (using, for \ +example, an ACK scan). +<b>open|filtered: (ofp:)</b> - Ports in the \"open|filtered\" state. +<b>closed|filtered: (cfp:)</b> - Ports in the \"closed|filtered\" state. +<b>service: (s:)</b> - All service-related fields. +<b>inroute: (ir:)</b> - Matches a router in the scan's traceroute output. +""") diff --git a/zenmap/zenmapGUI/Icons.py b/zenmap/zenmapGUI/Icons.py new file mode 100644 index 0000000..9d8ea9c --- /dev/null +++ b/zenmap/zenmapGUI/Icons.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib, GdkPixbuf + +import re +import os.path + +from zenmapCore.Paths import Path +from zenmapCore.UmitLogging import log + +icon_names = ( + # Operating Systems + 'default', + 'freebsd', + 'irix', + 'linux', + 'macosx', + 'openbsd', + 'redhat', + 'solaris', + 'ubuntu', + 'unknown', + 'win', + # Vulnerability Levels + 'vl_1', + 'vl_2', + 'vl_3', + 'vl_4', + 'vl_5') + +pixmap_path = Path.pixmaps_dir +if pixmap_path: + # This is a generator that returns file names for pixmaps in the order they + # should be tried. + def get_pixmap_file_names(icon_name, size): + yield '%s_%s.png' % (icon_name, size) + + iconfactory = Gtk.IconFactory() + for icon_name in icon_names: + for type, size in (('icon', '32'), ('logo', '75')): + key = '%s_%s' % (icon_name, type) + # Look for a usable image file. + for file_name in get_pixmap_file_names(icon_name, size): + file_path = os.path.join(pixmap_path, file_name) + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) + break + except GLib.GError: + # Try again. + pass + else: + log.warn('Could not find the icon for %s at ' + 'any of (%s) in %s' % ( + icon_name, + ', '.join(get_pixmap_file_names(icon_name, size)), + pixmap_path)) + continue + iconset = Gtk.IconSet(pixbuf=pixbuf) + iconfactory.add(key, iconset) + log.debug('Register %s icon name for file %s' % (key, file_path)) + iconfactory.add_default() + + +def get_os_icon(host): + osmatch = host.get_best_osmatch() + if osmatch and osmatch['osclasses']: + osclass = osmatch['osclasses'][0] + else: + osclass = None + + if osclass and osmatch: + return get_os(osclass['osfamily'], osmatch['name'], 'icon') + else: + return get_os(None, None, 'icon') + + +def get_os_logo(host): + osmatch = host.get_best_osmatch() + if osmatch and osmatch['osclasses']: + osclass = osmatch['osclasses'][0] + else: + osclass = None + + if osclass and osmatch: + return get_os(osclass['osfamily'], osmatch['name'], 'logo') + else: + return get_os(None, None, 'logo') + + +def get_os(osfamily, osmatch, type): + if osfamily: + if osfamily == 'Linux': + if re.findall("ubuntu", osmatch.lower()): + # Ubuntu icon + return 'ubuntu_%s' % type + elif re.findall("red hat", osmatch.lower()): + # RedHat icon + return 'redhat_%s' % type + else: + # Generic Linux icon + return 'linux_%s' % type + elif osfamily == 'Windows': + # Windows icon + return 'win_%s' % type + elif osfamily == 'OpenBSD': + # OpenBSD icon + return 'openbsd_%s' % type + elif osfamily == 'FreeBSD': + # FreeBSD icon + return 'freebsd_%s' % type + elif osfamily == 'NetBSD': + # NetBSD icon + return 'default_%s' % type + elif osfamily == 'Solaris': + # Solaris icon + return 'solaris_%s' % type + elif osfamily == 'OpenSolaris': + # OpenSolaris icon + return 'solaris_%s' % type + elif osfamily == 'IRIX': + # Irix icon + return 'irix_%s' % type + elif osfamily == 'Mac OS X': + # Mac OS X icon + return 'macosx_%s' % type + elif osfamily == 'Mac OS': + # Mac OS icon + return 'macosx_%s' % type + else: + # Default OS icon + return 'default_%s' % type + else: + # Unknown OS icon + return 'unknown_%s' % type + + +def get_vulnerability_logo(open_ports): + open_ports = int(open_ports) + if open_ports < 3: + return 'vl_1_logo' + elif open_ports < 5: + return 'vl_2_logo' + elif open_ports < 7: + return 'vl_3_logo' + elif open_ports < 9: + return 'vl_4_logo' + else: + return 'vl_5_logo' diff --git a/zenmap/zenmapGUI/MainWindow.py b/zenmap/zenmapGUI/MainWindow.py new file mode 100644 index 0000000..12ae07e --- /dev/null +++ b/zenmap/zenmapGUI/MainWindow.py @@ -0,0 +1,919 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import sys +import os +from os.path import split, isfile, join, abspath + +# Prevent loading PyXML +import xml +xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] + +import xml.sax.saxutils + +from zenmapGUI.higwidgets.higwindows import HIGMainWindow +from zenmapGUI.higwidgets.higdialogs import HIGDialog, HIGAlertDialog +from zenmapGUI.higwidgets.higlabels import HIGEntryLabel +from zenmapGUI.higwidgets.higboxes import HIGHBox, HIGVBox + +import zenmapGUI.App +from zenmapGUI.FileChoosers import RESPONSE_OPEN_DIRECTORY, \ + ResultsFileChooserDialog, SaveResultsFileChooserDialog, \ + SaveToDirectoryChooserDialog +from zenmapGUI.ScanInterface import ScanInterface +from zenmapGUI.ProfileEditor import ProfileEditor +from zenmapGUI.About import About +from zenmapGUI.DiffCompare import DiffWindow +from zenmapGUI.SearchWindow import SearchWindow +from zenmapGUI.BugReport import BugReport + +from zenmapCore.Name import APP_DISPLAY_NAME, APP_DOCUMENTATION_SITE +from zenmapCore.Paths import Path +from zenmapCore.RecentScans import recent_scans +from zenmapCore.UmitLogging import log +import zenmapCore.I18N # lgtm[py/unused-import] +import zenmapGUI.Print +from zenmapCore.UmitConf import SearchConfig, is_maemo, WindowConfig, config_parser + +UmitScanWindow = None +hildon = None + +if is_maemo(): + import hildon + + class UmitScanWindow(hildon.Window): + def __init__(self): + hildon.Window.__init__(self) + self.set_resizable(False) + self.set_border_width(0) + self.vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + self.vbox.set_border_width(0) + +else: + class UmitScanWindow(HIGMainWindow): + def __init__(self): + HIGMainWindow.__init__(self) + self.vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + + +def can_print(): + """Return true if we have printing operations (PyGTK 2.10 or later) or + false otherwise.""" + try: + Gtk.PrintOperation + except AttributeError: + return False + else: + return True + + +class ScanWindow(UmitScanWindow): + def __init__(self): + UmitScanWindow.__init__(self) + + window = WindowConfig() + + self.set_title(_(APP_DISPLAY_NAME)) + self.move(window.x, window.y) + self.set_default_size(window.width, window.height) + + self.scan_interface = ScanInterface() + + self.main_accel_group = Gtk.AccelGroup() + + self.add_accel_group(self.main_accel_group) + + # self.vbox is a container for the menubar and the scan interface + self.add(self.vbox) + + self.connect('delete-event', self._exit_cb) + self._create_ui_manager() + self._create_menubar() + self._create_scan_interface() + + self._results_filechooser_dialog = None + self._about_dialog = None + + def _create_ui_manager(self): + """Creates the UI Manager and a default set of actions, and builds + the menus using those actions.""" + self.ui_manager = Gtk.UIManager() + + # See info on ActionGroup at: + # * http://www.pygtk.org/pygtk2reference/class-gtkactiongroup.html + # * http://www.gtk.org/api/2.6/gtk/GtkActionGroup.html + self.main_action_group = Gtk.ActionGroup.new('MainActionGroup') + + # See info on Action at: + # * http://www.pygtk.org/pygtk2reference/class-gtkaction.html + # * http://www.gtk.org/api/2.6/gtk/GtkAction.html + + # Each action tuple can go from 1 to six fields, example: + # ('Open Scan Results', -> Name of the action + # gtk.STOCK_OPEN, -> + # _('_Open Scan Results'), -> + # None, + # _('Open the results of a previous scan'), + # lambda x: True) + + self.main_actions = [ + # Top level + ('Scan', None, _('Sc_an'), None), + + ('Save Scan', + Gtk.STOCK_SAVE, + _('_Save Scan'), + None, + _('Save current scan results'), + self._save_scan_results_cb), + + ('Save All Scans to Directory', + Gtk.STOCK_SAVE, + _('Save All Scans to _Directory'), + "<Control><Alt>s", + _('Save all scans into a directory'), + self._save_to_directory_cb), + + ('Open Scan', + Gtk.STOCK_OPEN, + _('_Open Scan'), + None, + _('Open the results of a previous scan'), + self._load_scan_results_cb), + + ('Append Scan', + Gtk.STOCK_ADD, + _('_Open Scan in This Window'), + None, + _('Append a saved scan to the list of scans in this window.'), + self._append_scan_results_cb), + + + ('Tools', None, _('_Tools'), None), + + ('New Window', + Gtk.STOCK_NEW, + _('_New Window'), + "<Control>N", + _('Open a new scan window'), + self._new_scan_cb), + + ('Close Window', + Gtk.STOCK_CLOSE, + _('Close Window'), + "<Control>w", + _('Close this scan window'), + self._exit_cb), + + ('Print...', + Gtk.STOCK_PRINT, + _('Print...'), + None, + _('Print the current scan'), + self._print_cb), + + ('Quit', + Gtk.STOCK_QUIT, + _('Quit'), + "<Control>q", + _('Quit the application'), + self._quit_cb), + + ('New Profile', + Gtk.STOCK_JUSTIFY_LEFT, + _('New _Profile or Command'), + '<Control>p', + _('Create a new scan profile using the current command'), + self._new_scan_profile_cb), + + ('Search Scan', + Gtk.STOCK_FIND, + _('Search Scan Results'), + '<Control>f', + _('Search for a scan result'), + self._search_scan_result), + + ('Filter Hosts', + Gtk.STOCK_FIND, + _('Filter Hosts'), + '<Control>l', + _('Search for host by criteria'), + self._filter_cb), + + ('Edit Profile', + Gtk.STOCK_PROPERTIES, + _('_Edit Selected Profile'), + '<Control>e', + _('Edit selected scan profile'), + self._edit_scan_profile_cb), + + # Top Level + ('Profile', None, _('_Profile'), None), + + ('Compare Results', + Gtk.STOCK_DND_MULTIPLE, + _('Compare Results'), + "<Control>D", + _('Compare Scan Results using Diffies'), + self._load_diff_compare_cb), + + + # Top Level + ('Help', None, _('_Help'), None), + + ('Report a bug', + Gtk.STOCK_DIALOG_INFO, + _('_Report a bug'), + '<Control>b', + _("Report a bug"), + self._show_bug_report + ), + + ('About', + Gtk.STOCK_ABOUT, + _('_About'), + None, + _("About %s") % APP_DISPLAY_NAME, + self._show_about_cb + ), + + ('Show Help', + Gtk.STOCK_HELP, + _('_Help'), + None, + _('Shows the application help'), + self._help_cb), + ] + + # See info on UIManager at: + # * http://www.pygtk.org/pygtk2reference/class-gtkuimanager.html + # * http://www.gtk.org/api/2.6/gtk/GtkUIManager.html + + # UIManager supports UI "merging" and "unmerging". So, suppose there's + # no scan running or scan results opened, we should have a minimal + # interface. When we one scan running, we should "merge" the scan UI. + # When we get multiple tabs opened, we might merge the tab UI. + + # This is the default, minimal UI + self.default_ui = """<menubar> + <menu action='Scan'> + <menuitem action='New Window'/> + <menuitem action='Open Scan'/> + <menuitem action='Append Scan'/> + %s + <separator/> + <menuitem action='Save Scan'/> + <menuitem action='Save All Scans to Directory'/> + """ + if can_print(): + self.default_ui += """ + <separator/> + <menuitem action='Print...'/> + """ + self.default_ui += """ + <separator/> + <menuitem action='Close Window'/> + <menuitem action='Quit'/> + </menu> + + <menu action='Tools'> + <menuitem action='Compare Results'/> + <menuitem action='Search Scan'/> + <menuitem action='Filter Hosts'/> + </menu> + + <menu action='Profile'> + <menuitem action='New Profile'/> + <menuitem action='Edit Profile'/> + </menu> + + <menu action='Help'> + <menuitem action='Show Help'/> + <menuitem action='Report a bug'/> + <menuitem action='About'/> + </menu> + + </menubar> + """ + + self.get_recent_scans() + + self.main_action_group.add_actions(self.main_actions) + + for action in self.main_action_group.list_actions(): + action.set_accel_group(self.main_accel_group) + action.connect_accelerator() + + self.ui_manager.insert_action_group(self.main_action_group, 0) + self.ui_manager.add_ui_from_string(self.default_ui) + + def _show_bug_report(self, widget): + """Displays a 'How to report a bug' window.""" + bug = BugReport() + bug.show_all() + + def _search_scan_result(self, widget): + """Displays a search window.""" + search_window = SearchWindow( + self._load_search_result, self._append_search_result) + search_window.show_all() + + def _filter_cb(self, widget): + self.scan_interface.toggle_filter_bar() + + def _load_search_result(self, results): + """This function is passed as an argument to the SearchWindow.__init__ + method. When the user selects scans in the search window and clicks on + "Open", this function is called to load each of the selected scans into + a new window.""" + for result in results: + self._load(self.get_empty_interface(), + parsed_result=results[result][1]) + + def _append_search_result(self, results): + """This function is passed as an argument to the SearchWindow.__init__ + method. When the user selects scans in the search window and clicks on + "Append", this function is called to append the selected scans into the + current window.""" + for result in results: + self._load(self.scan_interface, parsed_result=results[result][1]) + + def store_result(self, scan_interface): + """Stores the network inventory into the database.""" + log.debug(">>> Saving result into database...") + try: + scan_interface.inventory.save_to_db() + except Exception as e: + alert = HIGAlertDialog( + message_format=_("Can't save to database"), + secondary_text=_("Can't store unsaved scans to the " + "recent scans database:\n%s") % str(e)) + alert.run() + alert.destroy() + log.debug(">>> Can't save result to database: %s." % str(e)) + + def get_recent_scans(self): + """Gets seven most recent scans and appends them to the default UI + definition.""" + r_scans = recent_scans.get_recent_scans_list() + new_rscan_xml = '' + + for scan in r_scans[:7]: + scan = scan.replace('\n', '') + if os.access(split(scan)[0], os.R_OK) and isfile(scan): + scan = scan.replace('\n', '') + new_rscan = ( + scan, None, scan, None, scan, self._load_recent_scan) + new_rscan_xml += "<menuitem action=%s/>\n" % ( + xml.sax.saxutils.quoteattr(scan)) + + self.main_actions.append(new_rscan) + + new_rscan_xml += "<separator />\n" + + self.default_ui %= new_rscan_xml + + def _create_menubar(self): + # Get and pack the menubar + menubar = self.ui_manager.get_widget('/menubar') + + if is_maemo(): + menu = Gtk.Menu() + for child in menubar.get_children(): + child.reparent(menu) + self.set_menu(menu) + menubar.destroy() + self.menubar = menu + else: + self.menubar = menubar + self.vbox.pack_start(self.menubar, False, False, 0) + + self.menubar.show_all() + + def _create_scan_interface(self): + notebook = self.scan_interface.scan_result.scan_result_notebook + notebook.scans_list.append_button.connect( + "clicked", self._append_scan_results_cb) + notebook.nmap_output.connect("changed", self._displayed_scan_change_cb) + self._displayed_scan_change_cb(None) + self.scan_interface.show_all() + self.vbox.pack_start(self.scan_interface, True, True, 0) + + def show_open_dialog(self, title=None): + """Show a load file chooser and return the filename chosen.""" + if self._results_filechooser_dialog is None: + self._results_filechooser_dialog = ResultsFileChooserDialog( + title=title) + + filename = None + response = self._results_filechooser_dialog.run() + if response == Gtk.ResponseType.OK: + filename = self._results_filechooser_dialog.get_filename() + elif response == RESPONSE_OPEN_DIRECTORY: + filename = self._results_filechooser_dialog.get_filename() + + # Check if the selected filename is a directory. If not, we take + # only the directory part of the path, omitting the actual name of + # the selected file. + if filename is not None and not os.path.isdir(filename): + filename = os.path.dirname(filename) + + self._results_filechooser_dialog.hide() + return filename + + def _load_scan_results_cb(self, p): + """'Open Scan' callback function. Displays a file chooser dialog and + loads the scan from the selected file or from the selected + directory.""" + filename = self.show_open_dialog(p.get_name()) + if filename is not None: + scan_interface = self.get_empty_interface() + if os.path.isdir(filename): + self._load_directory(scan_interface, filename) + else: + self._load(scan_interface, filename) + + def _append_scan_results_cb(self, p): + """'Append Scan' callback function. Displays a file chooser dialog and + appends the scan from the selected file into the current window.""" + filename = self.show_open_dialog(p.get_name()) + if filename is not None: + if os.path.isdir(filename): + self._load_directory(self.scan_interface, filename) + else: + self._load(self.scan_interface, filename) + + def _displayed_scan_change_cb(self, widget): + """Called when the currently shown scan output is changed.""" + # Set the Print... menu item sensitive if there is something to print. + widget = self.ui_manager.get_widget("/ui/menubar/Scan/Print...") + if widget is None: + # Don't have a Print menu item for lack of support. + return + entry = self.scan_interface.scan_result.scan_result_notebook.nmap_output.get_active_entry() # noqa + widget.set_sensitive(entry is not None) + + def _load_recent_scan(self, widget): + """A helper function for loading a recent scan directly from the + menu.""" + self._load(self.get_empty_interface(), widget.get_name()) + + def _load(self, scan_interface, filename=None, parsed_result=None): + """Loads the scan from a file or from a parsed result into the given + scan interface.""" + if not (filename or parsed_result): + return None + + if filename: + # Load scan result from file + log.debug(">>> Loading file: %s" % filename) + try: + # Parse result + scan_interface.load_from_file(filename) + except Exception as e: + alert = HIGAlertDialog(message_format=_('Error loading file'), + secondary_text=str(e)) + alert.run() + alert.destroy() + return + scan_interface.saved_filename = filename + elif parsed_result: + # Load scan result from parsed object + scan_interface.load_from_parsed_result(parsed_result) + + def _load_directory(self, scan_interface, directory): + for file in os.listdir(directory): + if os.path.isdir(os.path.join(directory, file)): + continue + self._load(scan_interface, filename=os.path.join(directory, file)) + + def _save_scan_results_cb(self, widget): + """'Save Scan' callback function. If it's OK to save the scan, it + displays a 'Save File' dialog and saves the scan. If not, it displays + an appropriate alert dialog.""" + num_scans = len(self.scan_interface.inventory.get_scans()) + if num_scans == 0: + alert = HIGAlertDialog( + message_format=_('Nothing to save'), + secondary_text=_( + 'There are no scans with results to be saved. ' + 'Run a scan with the "Scan" button first.')) + alert.run() + alert.destroy() + return + num_scans_running = self.scan_interface.num_scans_running() + if num_scans_running > 0: + if num_scans_running == 1: + text = _("There is a scan still running. " + "Wait until it finishes and then save.") + else: + text = _("There are %u scans still running. Wait until they " + "finish and then save.") % num_scans_running + alert = HIGAlertDialog(message_format=_('Scan is running'), + secondary_text=text) + alert.run() + alert.destroy() + return + + # If there's more than one scan in the inventory, display a warning + # dialog saying that only the most recent scan will be saved + selected = 0 + if num_scans > 1: + #text = _("You have %u scans loaded in the current view. " + # "Only the most recent scan will be saved." % num_scans) + #alert = HIGAlertDialog( + # message_format=_("More than one scan loaded"), + # secondary_text=text) + #alert.run() + #alert.destroy() + dlg = HIGDialog( + title="Choose a scan to save", + parent=self, + flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) + dlg.vbox.pack_start(Gtk.Label.new( + "You have %u scans loaded in the current view.\n" + "Select the scan which you would like to save." % num_scans), + False, True, 0) + scan_combo = Gtk.ComboBoxText() + for scan in self.scan_interface.inventory.get_scans(): + scan_combo.append_text(scan.nmap_command) + scan_combo.set_active(0) + dlg.vbox.pack_start(scan_combo, False, True, 0) + dlg.vbox.show_all() + if dlg.run() == Gtk.ResponseType.OK: + selected = scan_combo.get_active() + dlg.destroy() + else: + dlg.destroy() + return + + # Show the dialog to choose the path to save scan result + self._save_results_filechooser_dialog = \ + SaveResultsFileChooserDialog(title=_('Save Scan')) + # Supply a default file name if this scan was previously saved. + if self.scan_interface.saved_filename: + self._save_results_filechooser_dialog.set_filename( + self.scan_interface.saved_filename) + + response = self._save_results_filechooser_dialog.run() + + filename = None + if (response == Gtk.ResponseType.OK): + filename = self._save_results_filechooser_dialog.get_filename() + format = self._save_results_filechooser_dialog.get_format() + # add .xml to filename if there is no other extension + if filename.find('.') == -1: + filename += ".xml" + self._save(self.scan_interface, filename, selected, format) + + self._save_results_filechooser_dialog.destroy() + self._save_results_filechooser_dialog = None + + def _save_to_directory_cb(self, widget): + if self.scan_interface.empty: + alert = HIGAlertDialog(message_format=_('Nothing to save'), + secondary_text=_('\ +This scan has not been run yet. Start the scan with the "Scan" button first.')) + alert.run() + alert.destroy() + return + num_scans_running = self.scan_interface.num_scans_running() + if num_scans_running > 0: + if num_scans_running == 1: + text = _("There is a scan still running. " + "Wait until it finishes and then save.") + else: + text = _("There are %u scans still running. Wait until they " + "finish and then save.") % num_scans_running + alert = HIGAlertDialog(message_format=_('Scan is running'), + secondary_text=text) + alert.run() + alert.destroy() + return + + # We have multiple scans in our network inventory, so we need to + # display a directory chooser dialog + dir_chooser = SaveToDirectoryChooserDialog( + title=_("Choose a directory to save scans into")) + if dir_chooser.run() == Gtk.ResponseType.OK: + self._save_all(self.scan_interface, dir_chooser.get_filename()) + dir_chooser.destroy() + + def _about_cb_response(self, dialog, response_id): + if response_id == Gtk.ResponseType.DELETE_EVENT: + self._about_dialog = None + else: + self._about_dialog.hide() + + def _show_about_cb(self, widget): + if self._about_dialog is None: + self._about_dialog = About() + self._about_dialog.connect("response", self._about_cb_response) + self._about_dialog.present() + + def _save_all(self, scan_interface, directory): + """Saves all scans in saving_page's inventory to a given directory. + Displays an alert dialog if the save fails.""" + try: + filenames = scan_interface.inventory.save_to_dir(directory) + for scan in scan_interface.inventory.get_scans(): + scan.unsaved = False + except Exception as ex: + alert = HIGAlertDialog(message_format=_('Can\'t save file'), + secondary_text=str(ex)) + alert.run() + alert.destroy() + else: + scan_interface.saved_filename = directory + + # Saving recent scan information + try: + for filename in filenames: + recent_scans.add_recent_scan(filename) + recent_scans.save() + except (OSError, IOError) as e: + alert = HIGAlertDialog( + message_format=_( + "Can't save recent scan information"), + secondary_text=_( + "Can't open file to write.\n%s") % str(e)) + alert.run() + alert.destroy() + + def _save(self, scan_interface, saved_filename, selected_index, + format="xml"): + """Saves the scan into a file with a given filename. Displays an alert + dialog if the save fails.""" + log.debug(">>> File being saved: %s" % saved_filename) + try: + scan_interface.inventory.save_to_file( + saved_filename, selected_index, format) + scan_interface.inventory.get_scans()[selected_index].unsaved = False # noqa + except (OSError, IOError) as e: + alert = HIGAlertDialog( + message_format=_("Can't save file"), + secondary_text=_("Can't open file to write.\n%s") % str(e)) + alert.run() + alert.destroy() + else: + scan_interface.saved_filename = saved_filename + + log.debug(">>> Changes on page? %s" % scan_interface.changed) + log.debug(">>> File saved at: %s" % scan_interface.saved_filename) + + if format == "xml": + # Saving recent scan information + try: + recent_scans.add_recent_scan(saved_filename) + recent_scans.save() + except (OSError, IOError) as e: + alert = HIGAlertDialog( + message_format=_( + "Can't save recent scan information"), + secondary_text=_( + "Can't open file to write.\n%s") % str(e)) + alert.run() + alert.destroy() + + def get_empty_interface(self): + """Return this window if it is empty, otherwise create and return a new + one.""" + if self.scan_interface.empty: + return self.scan_interface + return self._new_scan_cb().scan_interface + + def _new_scan_cb(self, widget=None, data=None): + """Create a new scan window.""" + w = zenmapGUI.App.new_window() + w.show_all() + return w + + def _new_scan_profile_cb(self, p): + pe = ProfileEditor( + command=self.scan_interface.command_toolbar.command, + deletable=False) + pe.set_scan_interface(self.scan_interface) + pe.show_all() + + def _edit_scan_profile_cb(self, p): + pe = ProfileEditor( + profile_name=self.scan_interface.toolbar.selected_profile, + deletable=True, overwrite=True) + pe.set_scan_interface(self.scan_interface) + pe.show_all() + + def _help_cb(self, action): + self.show_help() + + def _exit_cb(self, *args): + """Closes the window, prompting for confirmation if necessary. If one + of the tabs couldn't be closed, the function returns True and doesn't + exit the application.""" + if self.scan_interface.changed: + log.debug("Found changes on closing window") + dialog = HIGDialog( + buttons=(_('Close anyway'), + Gtk.ResponseType.CLOSE, Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL)) + + alert = HIGEntryLabel('<b>%s</b>' % _("Unsaved changes")) + + text = HIGEntryLabel(_("The given scan has unsaved changes.\n" + "What do you want to do?")) + hbox = HIGHBox() + hbox.set_border_width(5) + hbox.set_spacing(12) + + vbox = HIGVBox() + vbox.set_border_width(5) + vbox.set_spacing(12) + + image = Gtk.Image() + image.set_from_stock( + Gtk.STOCK_DIALOG_QUESTION, Gtk.IconSize.DIALOG) + + vbox.pack_start(alert, True, True, 0) + vbox.pack_start(text, True, True, 0) + hbox.pack_start(image, True, True, 0) + hbox.pack_start(vbox, True, True, 0) + + dialog.vbox.pack_start(hbox, True, True, 0) + dialog.vbox.show_all() + + response = dialog.run() + dialog.destroy() + + if response == Gtk.ResponseType.CANCEL: + return True + + search_config = SearchConfig() + if search_config.store_results: + self.store_result(self.scan_interface) + + elif self.scan_interface.num_scans_running() > 0: + log.debug("Trying to close a window with a running scan") + dialog = HIGDialog( + buttons=(_('Close anyway'), + Gtk.ResponseType.CLOSE, Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL)) + + alert = HIGEntryLabel('<b>%s</b>' % _("Trying to close")) + + text = HIGEntryLabel(_( + "The window you are trying to close has a scan running in " + "the background.\nWhat do you want to do?")) + hbox = HIGHBox() + hbox.set_border_width(5) + hbox.set_spacing(12) + + vbox = HIGVBox() + vbox.set_border_width(5) + vbox.set_spacing(12) + + image = Gtk.Image() + image.set_from_stock( + Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.DIALOG) + + vbox.pack_start(alert, True, True, 0) + vbox.pack_start(text, True, True, 0) + hbox.pack_start(image, True, True, 0) + hbox.pack_start(vbox, True, True, 0) + + dialog.vbox.pack_start(hbox, True, True, 0) + dialog.vbox.show_all() + + response = dialog.run() + dialog.destroy() + + if response == Gtk.ResponseType.CLOSE: + self.scan_interface.kill_all_scans() + elif response == Gtk.ResponseType.CANCEL: + return True + + window = WindowConfig() + window.x, window.y = self.get_position() + window.width, window.height = self.get_size() + window.save_changes() + if config_parser.failed: + alert = HIGAlertDialog( + message_format=_("Can't save Zenmap configuration"), + # newline before path to help avoid weird line wrapping + secondary_text=_( + 'An error occurred when saving to\n%s' + '\nThe error was: %s.' + ) % (Path.user_config_file, config_parser.failed)) + alert.run() + alert.destroy() + + self.destroy() + + return False + + def _print_cb(self, *args): + """Show a print dialog.""" + entry = self.scan_interface.scan_result.scan_result_notebook.nmap_output.get_active_entry() # noqa + if entry is None: + return False + zenmapGUI.Print.run_print_operation( + self.scan_interface.inventory, entry) + + def _quit_cb(self, *args): + """Close all open windows.""" + for window in zenmapGUI.App.open_windows[:]: + window.present() + if window._exit_cb(): + break + + def _load_diff_compare_cb(self, widget=None, extra=None): + """Loads all active scans into a dictionary, passes it to the + DiffWindow constructor, and then displays the 'Compare Results' + window.""" + self.diff_window = DiffWindow( + self.scan_interface.inventory.get_scans()) + self.diff_window.show_all() + + + def show_help(self): + import urllib.request + import webbrowser + + doc_path = abspath(join(Path.docs_dir, "help.html")) + url = "file:" + urllib.request.pathname2url(doc_path) + + try: + webbrowser.open(url, new=2) + except OSError as e: + d = HIGAlertDialog(parent=self, + message_format=_("Can't find documentation files"), + secondary_text=_("""\ +There was an error loading the documentation file %s (%s). See the \ +online documentation at %s.\ +""") % (doc_path, str(e), APP_DOCUMENTATION_SITE)) + d.run() + d.destroy() + +if __name__ == '__main__': + w = ScanWindow() + w.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/NmapOutputProperties.py b/zenmap/zenmapGUI/NmapOutputProperties.py new file mode 100644 index 0000000..279e7ff --- /dev/null +++ b/zenmap/zenmapGUI/NmapOutputProperties.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk + +import zenmapCore.I18N # lgtm[py/unused-import] +from zenmapCore.UmitConf import NmapOutputHighlight + +from zenmapGUI.higwidgets.higdialogs import HIGDialog +from zenmapGUI.higwidgets.hignotebooks import HIGNotebook +from zenmapGUI.higwidgets.higboxes import HIGVBox +from zenmapGUI.higwidgets.higtables import HIGTable +from zenmapGUI.higwidgets.higlabels import HIGEntryLabel +from zenmapGUI.higwidgets.higbuttons import HIGButton, HIGToggleButton + + +class NmapOutputProperties(HIGDialog): + def __init__(self, nmap_output_view): + HIGDialog.__init__(self, _("Nmap Output Properties"), + buttons=(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)) + + self.nmap_highlight = NmapOutputHighlight() + + self.__create_widgets() + self.__pack_widgets() + self.highlight_tab() + + self.vbox.show_all() + + def __create_widgets(self): + self.properties_notebook = HIGNotebook() + + def __pack_widgets(self): + self.vbox.pack_start(self.properties_notebook, True, True, 0) + + def highlight_tab(self): + # Creating highlight tab main box + self.highlight_main_vbox = HIGVBox() + + # Creating highlight tab main table + self.highlight_main_table = HIGTable() + self.highlight_main_table.set_border_width(6) + + ############# + # Properties: + self.property_names = {"details": [_("details"), "MAC Address:"], + "port_list": [_("port listing title"), + "PORT STATE SERVICE"], + "open_port": [_("open port"), + "22/tcp open ssh"], + "closed_port": [_("closed port"), + "70/tcp closed gopher"], + "filtered_port": [_("filtered port"), + "80/tcp filtered http"], + "date": [_("date"), "2006-05-26 11:14 BRT"], + "hostname": [_("hostname"), "scanme.nmap.org"], + "ip": ["ip", "127.0.0.1"]} + + for p in self.property_names: + settings = self.nmap_highlight.__getattribute__(p) + + self.property_names[p].append(settings[0]) + self.property_names[p].append(settings[1]) + self.property_names[p].append(settings[2]) + self.property_names[p].append(Gdk.Color(*settings[3])) + self.property_names[p].append(Gdk.Color(*settings[4])) + self.property_names[p].append(settings[5]) + + # Creating properties and related widgets and attaching it to main + # table + y1 = 0 + y2 = 1 + for p in self.property_names: + hp = HighlightProperty(p, self.property_names[p]) + self.highlight_main_table.attach( + hp.property_name_label, 0, 1, y1, y2) + self.highlight_main_table.attach(hp.example_label, 1, 2, y1, y2) + self.highlight_main_table.attach(hp.bold_tg_button, 2, 3, y1, y2) + self.highlight_main_table.attach(hp.italic_tg_button, 3, 4, y1, y2) + self.highlight_main_table.attach( + hp.underline_tg_button, 4, 5, y1, y2) + self.highlight_main_table.attach( + hp.text_color_button, 5, 6, y1, y2) + self.highlight_main_table.attach( + hp.highlight_color_button, 6, 7, y1, y2) + + # Setting example styles and colors + hp.update_example() + + self.property_names[p].append(hp) + + y1 += 1 + y2 += 1 + + # Packing main table into main vbox + self.highlight_main_vbox.pack_start(self.highlight_main_table, True, True, 0) + + # Adding color tab + self.properties_notebook.append_page( + self.highlight_main_vbox, + Gtk.Label.new(_("Highlight definitions"))) + + +class HighlightProperty(object): + def __init__(self, property_name, property): + self.__create_widgets() + + self.property_name = property_name + + self.property_label = property[0].capitalize() + self.example = property[1] + self.bold = property[2] + self.italic = property[3] + self.underline = property[4] + + self.text_color = property[5] + self.highlight_color = property[6] + + self.__connect_buttons() + + def __create_widgets(self): + self.property_name_label = HIGEntryLabel("") + self.example_label = HIGEntryLabel("") + self.bold_tg_button = HIGToggleButton("", Gtk.STOCK_BOLD) + self.italic_tg_button = HIGToggleButton("", Gtk.STOCK_ITALIC) + self.underline_tg_button = HIGToggleButton("", Gtk.STOCK_UNDERLINE) + self.text_color_button = HIGButton( + _("Text"), stock=Gtk.STOCK_SELECT_COLOR) + self.highlight_color_button = HIGButton( + _("Highlight"), stock=Gtk.STOCK_SELECT_COLOR) + + def __connect_buttons(self): + self.bold_tg_button.connect("toggled", self.update_example) + self.italic_tg_button.connect("toggled", self.update_example) + self.underline_tg_button.connect("toggled", self.update_example) + + self.text_color_button.connect("clicked", self.text_color_dialog) + self.highlight_color_button.connect( + "clicked", self.highlight_color_dialog) + + #################################### + # Text color dialog + + def text_color_dialog(self, widget): + color_dialog = Gtk.ColorSelectionDialog.new( + "%s %s" % (self.label, _("text color"))) + color_dialog.get_color_selection().set_current_color(self.text_color) + + color_dialog.props.ok_button.connect( + "clicked", self.text_color_dialog_ok, color_dialog) + color_dialog.props.cancel_button.connect( + "clicked", self.text_color_dialog_cancel, color_dialog) + color_dialog.connect( + "delete-event", self.text_color_dialog_close, color_dialog) + + color_dialog.run() + + def text_color_dialog_ok(self, widget, color_dialog): + self.text_color = color_dialog.get_color_selection().get_current_color() + color_dialog.destroy() + self.update_example() + + def text_color_dialog_cancel(self, widget, color_dialog): + color_dialog.destroy() + + def text_color_dialog_close(self, widget, extra, color_dialog): + color_dialog.destroy() + + ######################################### + # Highlight color dialog + def highlight_color_dialog(self, widget): + color_dialog = Gtk.ColorSelectionDialog.new( + "%s %s" % (self.property_name, _("highlight color"))) + color_dialog.get_color_selection().set_current_color(self.highlight_color) + + color_dialog.props.ok_button.connect( + "clicked", self.highlight_color_dialog_ok, color_dialog) + color_dialog.props.cancel_button.connect( + "clicked", self.highlight_color_dialog_cancel, + color_dialog) + color_dialog.connect( + "delete-event", self.highlight_color_dialog_close, + color_dialog) + + color_dialog.run() + + def highlight_color_dialog_ok(self, widget, color_dialog): + self.highlight_color = color_dialog.get_color_selection().get_current_color() + color_dialog.destroy() + self.update_example() + + def highlight_color_dialog_cancel(self, widget, color_dialog): + color_dialog.destroy() + + def highlight_color_dialog_close(self, widget, extra, color_dialog): + color_dialog.destroy() + + def update_example(self, widget=None): + label = self.example_label.get_text() + + # Bold verification + if self.bold_tg_button.get_active(): + label = "<b>" + label + "</b>" + + # Italic verification + if self.italic_tg_button.get_active(): + label = "<i>" + label + "</i>" + + # Underline verification + if self.underline_tg_button.get_active(): + label = "<u>" + label + "</u>" + + self.example_label.modify_fg(Gtk.StateType.NORMAL, self.text_color) + self.example_label.modify_bg(Gtk.StateType.NORMAL, self.highlight_color) + self.example_label.set_markup(label) + + def show_bold(self, widget): + self.example_label.set_markup("<>") + + def get_example(self): + return self.example_label.get_text() + + def set_example(self, example): + self.example_label.set_text(example) + + def get_bold(self): + if self.bold_tg_button.get_active(): + return 1 + return 0 + + def set_bold(self, bold): + self.bold_tg_button.set_active(bold) + + def get_italic(self): + if self.italic_tg_button.get_active(): + return 1 + return 0 + + def set_italic(self, italic): + self.italic_tg_button.set_active(italic) + + def get_underline(self): + if self.underline_tg_button.get_active(): + return 1 + return 0 + + def set_underline(self, underline): + self.underline_tg_button.set_active(underline) + + def get_label(self): + return self.property_name_label.get_text() + + def set_label(self, label): + self.property_name_label.set_text(label) + + label = property(get_label, set_label) + example = property(get_example, set_example) + bold = property(get_bold, set_bold) + italic = property(get_italic, set_italic) + underline = property(get_underline, set_underline) + +if __name__ == "__main__": + n = NmapOutputProperties(None) + n.run() + Gtk.main() diff --git a/zenmap/zenmapGUI/NmapOutputViewer.py b/zenmap/zenmapGUI/NmapOutputViewer.py new file mode 100644 index 0000000..349f9b9 --- /dev/null +++ b/zenmap/zenmapGUI/NmapOutputViewer.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, Pango, GLib + +import gobject +import re + +import zenmapCore.I18N # lgtm[py/unused-import] +from zenmapCore.UmitLogging import log +from zenmapCore.UmitConf import NmapOutputHighlight + +from zenmapGUI.NmapOutputProperties import NmapOutputProperties + + +class NmapOutputViewer(Gtk.Box): + HIGHLIGHT_PROPERTIES = ["details", "date", "hostname", "ip", "port_list", + "open_port", "closed_port", "filtered_port"] + + def __init__(self, refresh=1, stop=1): + self.nmap_highlight = NmapOutputHighlight() + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + + # Creating widgets + self.__create_widgets() + + # Setting scrolled window + self.__set_scrolled_window() + + # Setting text view + self.__set_text_view() + + buffer = self.text_view.get_buffer() + # The end mark is used to scroll to the bottom of the display. + self.end_mark = buffer.create_mark(None, buffer.get_end_iter(), False) + + self.refreshing = True + + # Adding widgets to the VBox + self.pack_start(self.scrolled, True, True, 0) + + # The NmapCommand instance, if any, whose output is shown in this + # display. + self.command_execution = None + # The position of the last read from the output stream. + self.output_file_pointer = None + + def __create_widgets(self): + # Creating widgets + self.scrolled = Gtk.ScrolledWindow() + self.text_view = Gtk.TextView() + + def __set_scrolled_window(self): + # Seting scrolled window + self.scrolled.set_border_width(5) + self.scrolled.add(self.text_view) + self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + def __set_text_view(self): + self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) + self.text_view.set_editable(False) + + self.tag_font = self.text_view.get_buffer().create_tag(None) + self.tag_font.set_property("family", "Monospace") + for property in self.HIGHLIGHT_PROPERTIES: + settings = self.nmap_highlight.__getattribute__(property) + tag = self.text_view.get_buffer().create_tag(property) + + if settings[0]: + tag.set_property("weight", Pango.Weight.HEAVY) + else: + tag.set_property("weight", Pango.Weight.NORMAL) + + if settings[1]: + tag.set_property("style", Pango.Style.ITALIC) + else: + tag.set_property("style", Pango.Style.NORMAL) + + if settings[2]: + tag.set_property("underline", Pango.Underline.SINGLE) + else: + tag.set_property("underline", Pango.Underline.NONE) + + text_color = settings[3] + highlight_color = settings[4] + + tag.set_property( + "foreground", Gdk.Color(*text_color).to_string()) + tag.set_property( + "background", Gdk.Color(*highlight_color).to_string()) + + def go_to_host(self, host): + """Go to host line on nmap output result""" + buff = self.text_view.get_buffer() + start_iter = buff.get_start_iter() + + found_tuple = start_iter.forward_search( + "\nNmap scan report for %s\n" % host, Gtk.TextSearchFlags.TEXT_ONLY + ) + if found_tuple is None: + return + + found = found_tuple[0] + if not found.forward_line(): + return + self.text_view.scroll_to_iter(found, 0, True, 0, 0) + + def show_output_properties(self, widget): + nmap_out_prop = NmapOutputProperties(self.text_view) + + nmap_out_prop.run() + + for prop in nmap_out_prop.property_names: + widget = nmap_out_prop.property_names[prop][8] + + wid_props = [] + + if widget.bold: + wid_props.append(1) + else: + wid_props.append(0) + + if widget.italic: + wid_props.append(1) + else: + wid_props.append(0) + + if widget.underline: + wid_props.append(1) + else: + wid_props.append(0) + + wid_props.append("(%s, %s, %s)" % (widget.text_color.red, + widget.text_color.green, + widget.text_color.blue)) + wid_props.append("(%s, %s, %s)" % (widget.highlight_color.red, + widget.highlight_color.green, + widget.highlight_color.blue)) + + self.nmap_highlight.__setattr__(widget.property_name, wid_props) + + nmap_out_prop.destroy() + self.nmap_highlight.save_changes() + self.apply_highlighting() + + def apply_highlighting(self, start_iter=None, end_iter=None): + buf = self.text_view.get_buffer() + + if start_iter is None: + start_iter = buf.get_start_iter() + else: + # Patterns are line-oriented; start on a line boundary. + start_iter.backward_line() + if end_iter is None: + end_iter = buf.get_end_iter() + + buf.apply_tag(self.tag_font, start_iter, end_iter) + + if not self.nmap_highlight.enable: + return + + text = buf.get_text(start_iter, end_iter, include_hidden_chars=True) + + for property in self.HIGHLIGHT_PROPERTIES: + settings = self.nmap_highlight.__getattribute__(property) + for m in re.finditer(settings[5], text, re.M): + m_start_iter = start_iter.copy() + m_start_iter.forward_chars(m.start()) + m_end_iter = start_iter.copy() + m_end_iter.forward_chars(m.end()) + buf.apply_tag_by_name(property, m_start_iter, m_end_iter) + + def show_nmap_output(self, output): + """Show the string (or unicode) output in the output display.""" + try: + self.text_view.get_buffer().set_text(output) + self.apply_highlighting() + except MemoryError: + self.show_large_output_message(self.command_execution) + + def set_command_execution(self, command): + """Set the live running command whose output is shown by this display. + The current output is extracted from the command object.""" + self.command_execution = command + if command is not None: + self.text_view.get_buffer().set_text("") + self.output_file_pointer = 0 + else: + self.output_file_pointer = None + self.refresh_output() + + def show_large_output_message(self, command=None): + buf = self.text_view.get_buffer() + try: + running = (command is not None and command.scan_state() is True) + except Exception: + running = False + complete = False + else: + complete = not running + if running: + buf.set_text("Warning: You have insufficient resources for Zenmap " + "to be able to display the complete output from Nmap here. \n" + "Zenmap will continue to run the scan to completion. However," + " some features of Zenmap might not work as expected.") + elif complete: + buf.set_text("Warning: You have insufficient resources for Zenmap " + "to be able to display the complete output from Nmap here. \n" + "The scan has completed. However, some features of Zenmap " + "might not work as expected.") + else: + buf.set_text("Warning: You have insufficient resources for Zenmap " + "to be able to display the complete output from Nmap here. \n" + "The scan has been stopped. Some features of Zenmap might not " + "work as expected.") + + def refresh_output(self, widget=None): + """Update the output from the latest output of the command associated + with this view, as set by set_command_execution. It has no effect if no + command has been set.""" + log.debug("Refresh nmap output") + + if self.command_execution is None: + return + + # Seek to the end of the most recent read. + self.command_execution.stdout_file.seek(self.output_file_pointer) + + try: + new_output = self.command_execution.stdout_file.read() + except MemoryError: + self.show_large_output_message(self.command_execution) + return + + self.output_file_pointer = self.command_execution.stdout_file.tell() + + v_adj = self.scrolled.get_vadjustment() + if new_output and v_adj is not None: + # Find out if the view is already scrolled to the bottom. + at_end = (v_adj.get_value() >= v_adj.get_upper() - v_adj.get_page_size()) + + buf = self.text_view.get_buffer() + prev_end_mark = buf.create_mark( + None, buf.get_end_iter(), left_gravity=True) + try: + buf.insert(buf.get_end_iter(), new_output) + # Highlight the new text. + self.apply_highlighting( + buf.get_iter_at_mark(prev_end_mark), + buf.get_end_iter()) + except MemoryError: + self.show_large_output_message(self.command_execution) + return + + if at_end: + # If we were already scrolled to the bottom, scroll back to the + # bottom again. Also do it in an idle handler in case the added + # text causes a scroll bar to appear and reflow the text, + # making the text a bit taller. + self.text_view.scroll_mark_onscreen(self.end_mark) + GLib.idle_add( + lambda: self.text_view.scroll_mark_onscreen( + self.end_mark)) diff --git a/zenmap/zenmapGUI/OptionBuilder.py b/zenmap/zenmapGUI/OptionBuilder.py new file mode 100644 index 0000000..2ef450b --- /dev/null +++ b/zenmap/zenmapGUI/OptionBuilder.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GObject + + +# Prevent loading PyXML +import xml +xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] + +from xml.dom import minidom + +from zenmapGUI.higwidgets.higlabels import HIGEntryLabel +from zenmapGUI.higwidgets.higbuttons import HIGButton + +from zenmapGUI.FileChoosers import AllFilesFileChooserDialog +from zenmapGUI.ProfileHelp import ProfileHelp + +from zenmapCore.NmapOptions import NmapOptions, split_quoted, join_quoted +import zenmapCore.I18N # lgtm[py/unused-import] +from zenmapGUI.ScriptInterface import ScriptInterface + + +def get_option_check_auxiliary_widget(option, ops, check): + if option in ("-sI", "-b", "--script", "--script-args", "--exclude", "-p", + "-D", "-S", "--source-port", "-e", "--ttl", "-iR", "--max-retries", + "--host-timeout", "--max-rtt-timeout", "--min-rtt-timeout", + "--initial-rtt-timeout", "--max-hostgroup", "--min-hostgroup", + "--max-parallelism", "--min-parallelism", "--max-scan-delay", + "--scan-delay", "-PA", "-PS", "-PU", "-PO", "-PY"): + return OptionEntry(option, ops, check) + elif option in ("-d", "-v"): + return OptionLevel(option, ops, check) + elif option in ("--excludefile", "-iL"): + return OptionFile(option, ops, check) + elif option in ("-A", "-O", "-sV", "-n", "-6", "-Pn", "-PE", "-PP", "-PM", + "-PB", "-sC", "--script-trace", "-F", "-f", "--packet-trace", "-r", + "--traceroute"): + return None + elif option in ("",): + return OptionExtras(option, ops, check) + else: + assert False, "Unknown option %s" % option + + +class OptionEntry(Gtk.Entry): + def __init__(self, option, ops, check): + Gtk.Entry.__init__(self) + self.option = option + self.ops = ops + self.check = check + self.connect("changed", self.changed_cb) + self.check.connect("toggled", self.check_toggled_cb) + self.update() + + def update(self): + if self.ops[self.option] is not None: + self.set_text(str(self.ops[self.option])) + self.check.set_active(True) + else: + self.set_text("") + self.check.set_active(False) + + def check_toggled_cb(self, check): + if check.get_active(): + self.ops[self.option] = self.get_text() + else: + self.ops[self.option] = None + + def changed_cb(self, widget): + self.check.set_active(True) + self.ops[self.option] = self.get_text() + + +class OptionExtras(Gtk.Entry): + def __init__(self, option, ops, check): + Gtk.Entry.__init__(self) + self.ops = ops + self.check = check + self.connect("changed", self.changed_cb) + self.check.connect("toggled", self.check_toggled_cb) + self.update() + + def update(self): + if len(self.ops.extras) > 0: + self.set_text(" ".join(self.ops.extras)) + self.check.set_active(True) + else: + self.set_text("") + self.check.set_active(False) + + def check_toggled_cb(self, check): + if check.get_active(): + self.ops.extras = [self.get_text()] + else: + self.ops.extras = [] + + def changed_cb(self, widget): + self.check.set_active(True) + self.ops.extras = [self.get_text()] + + +class OptionLevel(Gtk.SpinButton): + def __init__(self, option, ops, check): + adjustment = Gtk.Adjustment.new(0, 0, 10, 1, 0, 0) + Gtk.SpinButton.__init__(self, adjustment=adjustment, climb_rate=0.0, digits=0) + self.option = option + self.ops = ops + self.check = check + self.connect("changed", self.changed_cb) + self.check.connect("toggled", self.check_toggled_cb) + self.update() + + def update(self): + level = self.ops[self.option] + if level is not None and level > 0: + self.get_adjustment().set_value(int(level)) + self.check.set_active(True) + else: + self.get_adjustment().set_value(0) + self.check.set_active(False) + + def check_toggled_cb(self, check): + if check.get_active(): + self.ops[self.option] = int(self.get_adjustment().get_value()) + else: + self.ops[self.option] = 0 + + def changed_cb(self, widget): + self.check.set_active(True) + self.ops[self.option] = int(self.get_adjustment().get_value()) + + +class OptionFile(Gtk.Box): + __gsignals__ = { + "changed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()) + } + + def __init__(self, option, ops, check): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + + self.option = option + self.ops = ops + self.check = check + + self.entry = Gtk.Entry() + self.pack_start(self.entry, True, True, 0) + button = HIGButton(stock=Gtk.STOCK_OPEN) + self.pack_start(button, False, True, 0) + + button.connect("clicked", self.clicked_cb) + + self.entry.connect("changed", lambda x: self.emit("changed")) + self.entry.connect("changed", self.changed_cb) + self.check.connect("toggled", self.check_toggled_cb) + self.update() + + def update(self): + if self.ops[self.option] is not None: + self.entry.set_text(self.ops[self.option]) + self.check.set_active(True) + else: + self.entry.set_text("") + self.check.set_active(False) + + def check_toggled_cb(self, check): + if check.get_active(): + self.ops[self.option] = self.entry.get_text() + else: + self.ops[self.option] = None + + def changed_cb(self, widget): + self.check.set_active(True) + self.ops[self.option] = self.entry.get_text() + + def clicked_cb(self, button): + dialog = AllFilesFileChooserDialog(_("Choose file")) + if dialog.run() == Gtk.ResponseType.OK: + self.entry.set_text(dialog.get_filename()) + dialog.destroy() + + +class TargetEntry(Gtk.Entry): + def __init__(self, ops): + Gtk.Entry.__init__(self) + self.ops = ops + self.connect("changed", self.changed_cb) + self.update() + + def update(self): + self.set_text(" ".join(self.ops.target_specs)) + + def changed_cb(self, widget): + self.ops.target_specs = self.get_targets() + + def get_targets(self): + return split_quoted(self.get_text()) + + +class OptionTab(object): + def __init__(self, root_tab, ops, update_command, help_buf): + actions = {'target': self.__parse_target, + 'option_list': self.__parse_option_list, + 'option_check': self.__parse_option_check} + + self.ops = ops + self.update_command = update_command + self.help_buf = help_buf + + self.profilehelp = ProfileHelp() + self.notscripttab = False # assume every tab is scripting tab + self.widgets_list = [] + for option_element in root_tab.childNodes: + if (hasattr(option_element, "tagName") and + option_element.tagName in actions.keys()): + parse_func = actions[option_element.tagName] + widget = parse_func(option_element) + self.widgets_list.append(widget) + + def __parse_target(self, target_element): + label = _(target_element.getAttribute('label')) + label_widget = HIGEntryLabel(label) + target_widget = TargetEntry(self.ops) + target_widget.connect("changed", self.update_target) + return label_widget, target_widget + + def __parse_option_list(self, option_list_element): + children = option_list_element.getElementsByTagName('option') + + label_widget = HIGEntryLabel( + _(option_list_element.getAttribute('label'))) + option_list_widget = OptionList(self.ops) + + for child in children: + option = child.getAttribute('option') + argument = child.getAttribute('argument') + label = _(child.getAttribute('label')) + option_list_widget.append(option, argument, label) + self.profilehelp.add_label(option, label) + self.profilehelp.add_shortdesc( + option, _(child.getAttribute('short_desc'))) + self.profilehelp.add_example( + option, child.getAttribute('example')) + + option_list_widget.update() + + option_list_widget.connect("changed", self.update_list_option) + + return label_widget, option_list_widget + + def __parse_option_check(self, option_check): + option = option_check.getAttribute('option') + label = _(option_check.getAttribute('label')) + short_desc = _(option_check.getAttribute('short_desc')) + example = option_check.getAttribute('example') + + self.profilehelp.add_label(option, label) + self.profilehelp.add_shortdesc(option, short_desc) + self.profilehelp.add_example(option, example) + + check = OptionCheck(option, label) + auxiliary_widget = get_option_check_auxiliary_widget( + option, self.ops, check) + if auxiliary_widget is not None: + auxiliary_widget.connect("changed", self.update_auxiliary_widget) + auxiliary_widget.connect( + 'enter-notify-event', self.enter_notify_event_cb, option) + else: + check.set_active(not not self.ops[option]) + + check.connect('toggled', self.update_check, auxiliary_widget) + check.connect('enter-notify-event', self.enter_notify_event_cb, option) + + return check, auxiliary_widget + + def fill_table(self, table, expand_fill=True): + yopt = (0, Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL)[expand_fill] + for y, widget in enumerate(self.widgets_list): + if widget[1] is None: + table.attach(widget[0], 0, 2, y, y + 1, yoptions=yopt) + else: + table.attach(widget[0], 0, 1, y, y + 1, yoptions=yopt) + table.attach(widget[1], 1, 2, y, y + 1, yoptions=yopt) + + def update_auxiliary_widget(self, auxiliary_widget): + self.update_command() + + def update(self): + for check, auxiliary_widget in self.widgets_list: + if auxiliary_widget is not None: + auxiliary_widget.update() + else: + check.set_active(not not self.ops[check.option]) + + def update_target(self, entry): + self.ops.target_specs = entry.get_targets() + self.update_command() + + def update_check(self, check, auxiliary_widget): + if auxiliary_widget is None: + if check.get_active(): + self.ops[check.option] = True + else: + self.ops[check.option] = False + self.update_command() + + def update_list_option(self, widget): + if widget.last_selected: + self.ops[widget.last_selected] = None + + opt, arg, label = widget.list[widget.get_active()] + if opt: + if arg: + self.ops[opt] = arg + else: + self.ops[opt] = True + + widget.last_selected = opt + + self.show_help_for_option(opt) + + self.update_command() + + def show_help_for_option(self, option): + self.profilehelp.handler(option) + text = "" + if self.profilehelp.get_currentstate() == "Default": + text = "" + else: + text += self.profilehelp.get_label() + text += "\n\n" + text += self.profilehelp.get_shortdesc() + if self.profilehelp.get_example(): + text += "\n\nExample input:\n" + text += self.profilehelp.get_example() + self.help_buf.set_text(text) + + def enter_notify_event_cb(self, event, widget, option): + self.show_help_for_option(option) + + +class OptionBuilder(object): + def __init__(self, xml_file, ops, update_func, help_buf): + """ + xml_file is a UI description xml-file + ops is an NmapOptions instance + """ + xml_desc = open(xml_file) + self.xml = minidom.parse(xml_desc) + # Closing file to avoid problems with file descriptors + xml_desc.close() + + self.ops = ops + self.help_buf = help_buf + self.update_func = update_func + + self.root_tag = "interface" + + self.xml = self.xml.getElementsByTagName(self.root_tag)[0] + + self.groups = self.__parse_groups() + self.section_names = self.__parse_section_names() + self.tabs = self.__parse_tabs() + + def update(self): + for tab in self.tabs.values(): + tab.update() + + def __parse_section_names(self): + dic = {} + for group in self.groups: + grp = self.xml.getElementsByTagName(group)[0] + dic[group] = grp.getAttribute('label') + return dic + + def __parse_groups(self): + return [g_name.getAttribute('name') for g_name in + self.xml.getElementsByTagName('groups')[0].getElementsByTagName('group')] # noqa + + def __parse_tabs(self): + dic = {} + for tab_name in self.groups: + if tab_name != "Scripting": + dic[tab_name] = OptionTab( + self.xml.getElementsByTagName(tab_name)[0], self.ops, + self.update_func, self.help_buf) + dic[tab_name].notscripttab = True + else: + dic[tab_name] = ScriptInterface( + None, self.ops, self.update_func, self.help_buf) + return dic + + +class OptionList(Gtk.ComboBox): + def __init__(self, ops): + self.ops = ops + + self.list = Gtk.ListStore.new([str, str, str]) + Gtk.ComboBox.__init__(self, model=self.list) + + cell = Gtk.CellRendererText() + self.pack_start(cell, True) + self.add_attribute(cell, 'text', 2) + + self.last_selected = None + self.options = [] + + def update(self): + selected = 0 + for i, row in enumerate(self.list): + opt, arg = row[0], row[1] + if opt == "": + continue + if ((not arg and self.ops[opt]) or + (arg and str(self.ops[opt]) == arg)): + selected = i + self.set_active(selected) + + def append(self, option, argument, label): + opt = label + ops = NmapOptions() + if option is not None and option != "": + if argument: + ops[option] = argument + else: + ops[option] = True + opt += " (%s)" % join_quoted(ops.render()[1:]) + + self.list.append([option, argument, opt]) + self.options.append(option) + + +class OptionCheck(Gtk.CheckButton): + def __init__(self, option, label): + opt = label + if option is not None and option != "": + opt += " (%s)" % option + + Gtk.CheckButton.__init__(self, label=opt, use_underline=False) + + self.option = option diff --git a/zenmap/zenmapGUI/Print.py b/zenmap/zenmapGUI/Print.py new file mode 100644 index 0000000..a6b2f2f --- /dev/null +++ b/zenmap/zenmapGUI/Print.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +# This prints the normal (text) output of a single scan. Ideas for further +# development: +# +# Print the topology graphic. The graphic is already made with Cairo so the +# same code can be used to draw on the print context. +# +# Print in color with highlighting, like NmapOutputViewer. +# +# Add a header to each page with the Nmap command and page number. +# +# Add options to the print dialog to control the font, coloring, and anything +# else. This might go in a separate Print Setup dialog. + +import gi + +gi.require_version("Gtk", "3.0") +gi.require_version("PangoCairo", "1.0") +from gi.repository import Gtk, GLib, Pango, PangoCairo + +MONOSPACE_FONT_DESC = Pango.FontDescription("Monospace 12") + + +class PrintState(object): + """This is the userdatum passed to Gtk.PrintOperation callbacks.""" + + def __init__(self, inventory, entry): + """entry is a ScansListStoreEntry.""" + if entry.parsed: + # Finished scan. + output = entry.parsed.nmap_output + else: + # Still running, failed, or cancelled. + output = entry.command.get_output() + if not output: + output = "\n" + self.lines = output.splitlines() + + def begin_print(self, op, context): + """Calculates the number of printed pages.""" + # Typeset a dummy line to get the exact line height. + layout = context.create_pango_layout() + layout.set_font_description(MONOSPACE_FONT_DESC) + layout.set_text("dummy", -1) + line = layout.get_line(0) + # get_extents()[1].height is the height of the logical rectangle. + line_height = line.get_extents()[1].height / Pango.SCALE + + page_height = context.get_height() + self.lines_per_page = int(page_height / line_height) + op.set_n_pages((len(self.lines) - 1) // self.lines_per_page + 1) + + def draw_page(self, op, context, page_nr): + this_page_lines = self.lines[ + page_nr * self.lines_per_page: + (page_nr + 1) * self.lines_per_page] + layout = context.create_pango_layout() + # Do no wrapping. + layout.set_width(-1) + layout.set_font_description(MONOSPACE_FONT_DESC) + text = "\n".join(this_page_lines) + layout.set_text(text, -1) + + cr = context.get_cairo_context() + PangoCairo.show_layout(cr, layout) + + +def run_print_operation(inventory, entry): + op = Gtk.PrintOperation() + state = PrintState(inventory, entry) + op.connect("begin-print", state.begin_print) + op.connect("draw-page", state.draw_page) + try: + op.run(Gtk.PrintOperationAction.PRINT_DIALOG, None) + except GLib.GError: + # Canceling the print operation can result in the error + # GError: Error from StartDoc + # http://seclists.org/nmap-dev/2012/q4/161 + pass diff --git a/zenmap/zenmapGUI/ProfileCombo.py b/zenmap/zenmapGUI/ProfileCombo.py new file mode 100644 index 0000000..5795110 --- /dev/null +++ b/zenmap/zenmapGUI/ProfileCombo.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapCore.UmitConf import CommandProfile +import zenmapCore.I18N # lgtm[py/unused-import] + + +class ProfileCombo(Gtk.ComboBoxText, object): + def __init__(self): + Gtk.ComboBoxText.__init__(self, has_entry=True) + + self.completion = Gtk.EntryCompletion() + self.get_child().set_completion(self.completion) + self.completion.set_model(self.get_model()) + self.completion.set_text_column(0) + + self.update() + + def set_profiles(self, profiles): + self.remove_all() + + for command in profiles: + self.append_text(command) + + def update(self): + profile = CommandProfile() + profiles = profile.sections() + profiles.sort() + del(profile) + + self.set_profiles(profiles) + + def get_selected_profile(self): + return self.get_child().get_text() + + def set_selected_profile(self, profile): + self.get_child().set_text(profile) + + selected_profile = property(get_selected_profile, set_selected_profile) + +if __name__ == "__main__": + w = Gtk.Window() + p = ProfileCombo() + p.update() + w.add(p) + + w.connect("delete-event", lambda x, y: Gtk.main_quit()) + w.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/ProfileEditor.py b/zenmap/zenmapGUI/ProfileEditor.py new file mode 100644 index 0000000..5c374d9 --- /dev/null +++ b/zenmap/zenmapGUI/ProfileEditor.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.higwidgets.higwindows import HIGWindow +from zenmapGUI.higwidgets.higboxes import HIGVBox, HIGHBox, HIGSpacer, \ + hig_box_space_holder +from zenmapGUI.higwidgets.higlabels import HIGSectionLabel, HIGEntryLabel +from zenmapGUI.higwidgets.higscrollers import HIGScrolledWindow +from zenmapGUI.higwidgets.higtextviewers import HIGTextView +from zenmapGUI.higwidgets.higbuttons import HIGButton +from zenmapGUI.higwidgets.higtables import HIGTable +from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog, HIGDialog +from zenmapGUI.OptionBuilder import OptionBuilder +from zenmapCore.Paths import Path +from zenmapCore.UmitConf import CommandProfile +from zenmapCore.UmitLogging import log +import zenmapCore.I18N # lgtm[py/unused-import] +from zenmapCore.NmapOptions import NmapOptions + + +class ProfileEditor(HIGWindow): + def __init__(self, command=None, profile_name=None, + deletable=True, overwrite=False): + HIGWindow.__init__(self) + self.connect("delete_event", self.exit) + self.set_title(_('Profile Editor')) + self.set_position(Gtk.WindowPosition.CENTER) + + self.deletable = deletable + self.profile_name = profile_name + self.overwrite = overwrite + + # Used to block recursive updating of the command entry when the + # command entry causes the OptionBuilder widgets to change. + self.inhibit_command_update = False + + self.__create_widgets() + self.__pack_widgets() + + self.profile = CommandProfile() + + self.ops = NmapOptions() + if profile_name: + log.debug("Showing profile %s" % profile_name) + prof = self.profile.get_profile(profile_name) + + # Interface settings + self.profile_name_entry.set_text(profile_name) + self.profile_description_text.get_buffer().set_text( + prof['description']) + + command_string = prof['command'] + self.ops.parse_string(command_string) + if command: + self.ops.parse_string(command) + + self.option_builder = OptionBuilder( + Path.profile_editor, self.ops, + self.update_command, self.help_field.get_buffer()) + log.debug("Option groups: %s" % str(self.option_builder.groups)) + log.debug("Option section names: %s" % str( + self.option_builder.section_names)) + #log.debug("Option tabs: %s" % str(self.option_builder.tabs)) + + for tab in self.option_builder.groups: + self.__create_tab( + _(tab), + _(self.option_builder.section_names[tab]), + self.option_builder.tabs[tab]) + + self.update_command() + + def command_entry_changed_cb(self, widget): + command_string = self.command_entry.get_text() + self.ops.parse_string(command_string) + self.inhibit_command_update = True + self.option_builder.update() + self.inhibit_command_update = False + + def update_command(self): + """Regenerate and display the command.""" + if not self.inhibit_command_update: + # Block recursive updating of the OptionBuilder widgets when they + # cause a change in the command entry. + self.command_entry.handler_block(self.command_entry_changed_cb_id) + self.command_entry.set_text(self.ops.render_string()) + self.command_entry.handler_unblock( + self.command_entry_changed_cb_id) + + def update_help_name(self, widget, extra): + self.help_field.get_buffer().set_text( + "Profile name\n\nThis is how the profile will be identified " + "in the drop-down combo box in the scan tab.") + + def update_help_desc(self, widget, extra): + self.help_field.get_buffer().set_text( + "Description\n\nThe description is a full description of what " + "the scan does, which may be long.") + + def __create_widgets(self): + + ### + # Vertical box to keep 3 boxes + self.main_whole_box = HIGVBox() + + self.upper_box = HIGHBox() + self.middle_box = HIGHBox() + self.lower_box = HIGHBox() + + #self.main_vbox = HIGVBox() + self.command_entry = Gtk.Entry() + self.command_entry_changed_cb_id = self.command_entry.connect( + "changed", self.command_entry_changed_cb) + + self.scan_button = HIGButton(_("Scan")) + self.scan_button.connect("clicked", self.run_scan) + + self.notebook = Gtk.Notebook() + + # Profile info page + self.profile_info_vbox = HIGVBox() + self.profile_info_label = HIGSectionLabel(_('Profile Information')) + self.profile_name_label = HIGEntryLabel(_('Profile name')) + self.profile_name_label.set_line_wrap(False) + self.profile_name_entry = Gtk.Entry() + self.profile_name_entry.connect( + 'enter-notify-event', self.update_help_name) + self.profile_description_label = HIGEntryLabel(_('Description')) + self.profile_description_scroll = HIGScrolledWindow() + self.profile_description_scroll.set_border_width(0) + self.profile_description_text = HIGTextView() + self.profile_description_text.connect( + 'motion-notify-event', self.update_help_desc) + + # Buttons + self.buttons_hbox = HIGHBox() + + self.cancel_button = HIGButton(stock=Gtk.STOCK_CANCEL) + self.cancel_button.connect('clicked', self.exit) + + self.delete_button = HIGButton(stock=Gtk.STOCK_DELETE) + self.delete_button.connect('clicked', self.delete_profile) + + self.save_button = HIGButton(_("Save Changes"), stock=Gtk.STOCK_SAVE) + self.save_button.connect('clicked', self.save_profile) + + ### + self.help_vbox = HIGVBox() + self.help_label = HIGSectionLabel(_('Help')) + self.help_scroll = HIGScrolledWindow() + self.help_scroll.set_border_width(0) + self.help_field = HIGTextView() + self.help_field.set_cursor_visible(False) + self.help_field.set_left_margin(5) + self.help_field.set_editable(False) + self.help_vbox.set_size_request(200, -1) + ### + + def __pack_widgets(self): + + ### + self.add(self.main_whole_box) + + # Packing command entry to upper box + self.upper_box._pack_expand_fill(self.command_entry) + self.upper_box._pack_noexpand_nofill(self.scan_button) + + # Packing notebook (left) and help box (right) to middle box + self.middle_box._pack_expand_fill(self.notebook) + self.middle_box._pack_expand_fill(self.help_vbox) + + # Packing buttons to lower box + self.lower_box.pack_end(self.buttons_hbox, True, True, 0) + + # Packing the three vertical boxes to the main box + self.main_whole_box._pack_noexpand_nofill(self.upper_box) + self.main_whole_box._pack_expand_fill(self.middle_box) + self.main_whole_box._pack_noexpand_nofill(self.lower_box) + ### + + # Packing profile information tab on notebook + self.notebook.append_page( + self.profile_info_vbox, Gtk.Label.new(_('Profile'))) + self.profile_info_vbox.set_border_width(5) + table = HIGTable() + self.profile_info_vbox._pack_noexpand_nofill(self.profile_info_label) + self.profile_info_vbox._pack_expand_fill(HIGSpacer(table)) + + self.profile_description_scroll.add(self.profile_description_text) + + vbox_desc = HIGVBox() + vbox_desc._pack_noexpand_nofill(self.profile_description_label) + vbox_desc._pack_expand_fill(hig_box_space_holder()) + + vbox_ann = HIGVBox() + vbox_ann._pack_expand_fill(hig_box_space_holder()) + + table.attach( + self.profile_name_label, 0, 1, 0, 1, xoptions=0, yoptions=0) + table.attach(self.profile_name_entry, 1, 2, 0, 1, yoptions=0) + table.attach(vbox_desc, 0, 1, 1, 2, xoptions=0) + table.attach(self.profile_description_scroll, 1, 2, 1, 2) + + # Packing buttons on button_hbox + self.buttons_hbox._pack_expand_fill(hig_box_space_holder()) + if self.deletable: + self.buttons_hbox._pack_noexpand_nofill(self.delete_button) + self.buttons_hbox._pack_noexpand_nofill(self.cancel_button) + self.buttons_hbox._pack_noexpand_nofill(self.save_button) + + self.buttons_hbox.set_border_width(5) + self.buttons_hbox.set_spacing(6) + + ### + self.help_vbox._pack_noexpand_nofill(self.help_label) + self.help_vbox._pack_expand_fill(self.help_scroll) + self.help_scroll.add(self.help_field) + self.help_vbox.set_border_width(1) + self.help_vbox.set_spacing(1) + ### + + def __create_tab(self, tab_name, section_name, tab): + log.debug(">>> Tab name: %s" % tab_name) + log.debug(">>>Creating profile editor section: %s" % section_name) + vbox = HIGVBox() + if tab.notscripttab: # if notscripttab is set + table = HIGTable() + table.set_row_spacings(2) + section = HIGSectionLabel(section_name) + vbox._pack_noexpand_nofill(section) + vbox._pack_noexpand_nofill(HIGSpacer(table)) + vbox.set_border_width(5) + tab.fill_table(table, True) + else: + hbox = tab.get_hmain_box() + vbox.pack_start(hbox, True, True, 0) + self.notebook.append_page(vbox, Gtk.Label.new(tab_name)) + + def save_profile(self, widget): + if self.overwrite: + self.profile.remove_profile(self.profile_name) + profile_name = self.profile_name_entry.get_text() + if profile_name == '': + alert = HIGAlertDialog( + message_format=_('Unnamed profile'), + secondary_text=_( + 'You must provide a name for this profile.')) + alert.run() + alert.destroy() + + self.profile_name_entry.grab_focus() + + return None + + command = self.ops.render_string() + + buf = self.profile_description_text.get_buffer() + description = buf.get_text( + buf.get_start_iter(), buf.get_end_iter(), include_hidden_chars=True) + + try: + self.profile.add_profile( + profile_name, + command=command, + description=description) + except ValueError: + alert = HIGAlertDialog( + message_format=_('Disallowed profile name'), + secondary_text=_('Sorry, the name "%s" is not allowed due ' + 'to technical limitations. (The underlying ' + 'ConfigParser used to store profiles does not allow ' + 'it.) Choose a different name.' % profile_name)) + alert.run() + alert.destroy() + return + + self.scan_interface.toolbar.profile_entry.update() + self.destroy() + + def clean_profile_info(self): + self.profile_name_entry.set_text('') + self.profile_description_text.get_buffer().set_text('') + + def set_scan_interface(self, interface): + self.scan_interface = interface + + def exit(self, *args): + self.destroy() + + def delete_profile(self, widget=None, extra=None): + if self.deletable: + dialog = HIGDialog(buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK, + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) + alert = HIGEntryLabel('<b>' + _("Deleting Profile") + '</b>') + text = HIGEntryLabel(_( + 'Your profile is going to be deleted! Click Ok to continue, ' + 'or Cancel to go back to Profile Editor.')) + hbox = HIGHBox() + hbox.set_border_width(5) + hbox.set_spacing(12) + + vbox = HIGVBox() + vbox.set_border_width(5) + vbox.set_spacing(12) + + image = Gtk.Image() + image.set_from_stock( + Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.DIALOG) + + vbox.pack_start(alert, True, True, 0) + vbox.pack_start(text, True, True, 0) + hbox.pack_start(image, True, True, 0) + hbox.pack_start(vbox, True, True, 0) + + dialog.vbox.pack_start(hbox, True, True, 0) + dialog.vbox.show_all() + + response = dialog.run() + dialog.destroy() + if response == Gtk.ResponseType.CANCEL: + return True + self.profile.remove_profile(self.profile_name) + + self.update_profile_entry() + self.destroy() + + def run_scan(self, widget=None): + command_string = self.command_entry.get_text() + self.scan_interface.command_toolbar.command = command_string + self.scan_interface.start_scan_cb() + self.exit() + + def update_profile_entry(self, widget=None, extra=None): + self.scan_interface.toolbar.profile_entry.update() + list = self.scan_interface.toolbar.profile_entry.get_model() + length = len(list) + if length > 0: + self.scan_interface.toolbar.profile_entry.set_active(0) + + +if __name__ == '__main__': + p = ProfileEditor() + p.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/ProfileHelp.py b/zenmap/zenmapGUI/ProfileHelp.py new file mode 100644 index 0000000..9baf04c --- /dev/null +++ b/zenmap/zenmapGUI/ProfileHelp.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +from zenmapCore.UmitLogging import log + + +class ProfileHelp: + def __init__(self, currentstate=None): + self.currentstate = "Default" + self.labels = {} + self.descs = {} + self.examples = {} + + def get_currentstate(self): + return self.currentstate + + def add_label(self, option_name, text): + self.labels[option_name] = text + + def get_label(self): + return self.labels.get(self.currentstate, "") + + def add_shortdesc(self, option_name, text): + self.descs[option_name] = text + + def get_shortdesc(self): + return self.descs.get(self.currentstate, "") + + def add_example(self, option_name, text): + self.examples[option_name] = text + + def get_example(self): + return self.examples.get(self.currentstate, "") + + def handler(self, whichLabel): + log.debug("whichLabel: %s" % whichLabel) + self.currentstate = whichLabel diff --git a/zenmap/zenmapGUI/ScanHostDetailsPage.py b/zenmap/zenmapGUI/ScanHostDetailsPage.py new file mode 100644 index 0000000..f275c26 --- /dev/null +++ b/zenmap/zenmapGUI/ScanHostDetailsPage.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.higwidgets.higexpanders import HIGExpander +from zenmapGUI.higwidgets.higboxes import HIGVBox, HIGHBox,\ + hig_box_space_holder +from zenmapGUI.higwidgets.higlabels import HIGEntryLabel +from zenmapGUI.higwidgets.higtables import HIGTable +from zenmapGUI.Icons import get_os_logo, get_vulnerability_logo + +import zenmapCore.I18N # lgtm[py/unused-import] + +na = _('Not available') + + +class ScanHostDetailsPage(HIGExpander): + def __init__(self, host): + HIGExpander.__init__(self, host.get_hostname()) + + self.host_details = HostDetails(host) + self.hbox._pack_expand_fill(self.host_details) + + +class HostDetails(HIGVBox): + def __init__(self, host): + HIGVBox.__init__(self) + + self.__create_widgets() + + self.set_os_image(get_os_logo(host)) + + self.set_vulnerability_image( + get_vulnerability_logo(host.get_open_ports())) + + self.set_host_status({'state': host.get_state(), + 'open': str(host.get_open_ports()), + 'filtered': str(host.get_filtered_ports()), + 'closed': str(host.get_closed_ports()), + 'scanned': str(host.get_scanned_ports()), + 'uptime': host.get_uptime()['seconds'], + 'lastboot': host.get_uptime()['lastboot']}) + + addresses = {} + if host.ip is not None: + addresses['ipv4'] = host.ip['addr'] + if host.ipv6 is not None: + addresses['ipv6'] = host.ipv6['addr'] + if host.mac is not None: + addresses['mac'] = host.mac['addr'] + self.set_addresses(addresses) + + self.set_hostnames(host.get_hostnames()) + + os = host.get_best_osmatch() + if os: + os['portsused'] = host.get_ports_used() + + self.set_os(os) + self.set_tcpseq(host.get_tcpsequence()) + self.set_ipseq(host.get_ipidsequence()) + self.set_tcptsseq(host.get_tcptssequence()) + self.set_comment(host.comment) + + def __create_widgets(self): + self.host_status_expander = Gtk.Expander.new( + '<b>' + _('Host Status') + '</b>') + self.address_expander = Gtk.Expander.new('<b>' + _('Addresses') + '</b>') + self.hostnames_expander = Gtk.Expander.new('<b>' + _('Hostnames') + '</b>') + self.os_expander = Gtk.Expander.new('<b>' + _('Operating System') + '</b>') + self.portsused_expander = Gtk.Expander.new( + '<b>' + _('Ports used') + '</b>') + self.osclass_expander = Gtk.Expander.new('<b>' + _('OS Classes') + '</b>') + self.tcp_expander = Gtk.Expander.new('<b>' + _('TCP Sequence') + '</b>') + self.ip_expander = Gtk.Expander.new('<b>' + _('IP ID Sequence') + '</b>') + self.tcpts_expander = Gtk.Expander.new( + '<b>' + _('TCP TS Sequence') + '</b>') + self.comment_expander = Gtk.Expander.new('<b>' + _('Comments') + '</b>') + self.os_image = Gtk.Image() + self.vulnerability_image = Gtk.Image() + + # Host Status expander + self.host_state_label = HIGEntryLabel(_('State:')) + self.info_host_state_label = HIGEntryLabel(na) + + self.open_label = HIGEntryLabel(_('Open ports:')) + self.info_open_ports = HIGEntryLabel(na) + + self.filtered_label = HIGEntryLabel(_('Filtered ports:')) + self.info_filtered_label = HIGEntryLabel(na) + + self.closed_label = HIGEntryLabel(_('Closed ports:')) + self.info_closed_ports = HIGEntryLabel(na) + + self.scanned_label = HIGEntryLabel(_('Scanned ports:')) + self.info_scanned_label = HIGEntryLabel(na) + + self.uptime_label = HIGEntryLabel(_('Up time:')) + self.info_uptime_label = HIGEntryLabel(na) + + self.lastboot_label = HIGEntryLabel(_('Last boot:')) + self.info_lastboot_label = HIGEntryLabel(na) + + # Addresses expander + self.ipv4_label = HIGEntryLabel('IPv4:') + self.info_ipv4_label = HIGEntryLabel(na) + + self.ipv6_label = HIGEntryLabel('IPv6:') + self.info_ipv6_label = HIGEntryLabel(na) + + self.mac_label = HIGEntryLabel('MAC:') + self.info_mac_label = HIGEntryLabel(na) + + self.vendor_label = HIGEntryLabel(_('Vendor:')) + self.info_vendor_label = HIGEntryLabel(na) + + def create_table_hbox(self): + table = HIGTable() + hbox = HIGHBox() + + hbox._pack_noexpand_nofill(hig_box_space_holder()) + hbox._pack_noexpand_nofill(table) + + return table, hbox + + def set_host_status(self, status): + self.host_status_expander.set_use_markup(True) + self.host_status_expander.set_expanded(True) + table, hbox = self.create_table_hbox() + + if ('state' in status and + status['state'] != ''): + self.info_host_state_label.set_text(status['state']) + + if ('open' in status and + status['open'] != ''): + self.info_open_ports.set_text(status['open']) + + if ('filtered' in status and + status['filtered'] != ''): + self.info_filtered_label.set_text(status['filtered']) + + if ('closed' in status and + status['closed'] != ''): + self.info_closed_ports.set_text(status['closed']) + + if ('scanned' in status and + status['scanned'] != ''): + self.info_scanned_label.set_text(status['scanned']) + + if ('uptime' in status and + status['uptime'] != ''): + self.info_uptime_label.set_text(status['uptime']) + + if ('lastboot' in status and + status['lastboot'] != ''): + self.info_lastboot_label.set_text(status['lastboot']) + + table.attach(self.host_state_label, 0, 1, 0, 1) + table.attach(self.info_host_state_label, 1, 2, 0, 1) + + table.attach(self.open_label, 0, 1, 1, 2) + table.attach(self.info_open_ports, 1, 2, 1, 2) + + table.attach(self.filtered_label, 0, 1, 2, 3) + table.attach(self.info_filtered_label, 1, 2, 2, 3) + + table.attach(self.closed_label, 0, 1, 3, 4) + table.attach(self.info_closed_ports, 1, 2, 3, 4) + + table.attach(self.scanned_label, 0, 1, 4, 5) + table.attach(self.info_scanned_label, 1, 2, 4, 5) + + table.attach(self.uptime_label, 0, 1, 5, 6) + table.attach(self.info_uptime_label, 1, 2, 5, 6) + + table.attach(self.lastboot_label, 0, 1, 6, 7) + table.attach(self.info_lastboot_label, 1, 2, 6, 7) + + table.attach(self.os_image, 2, 4, 0, 3, xoptions=Gtk.AttachOptions.EXPAND) + table.attach( + self.vulnerability_image, 2, 4, 4, 7, xoptions=Gtk.AttachOptions.EXPAND) + + table.set_col_spacing(1, 50) + + self.host_status_expander.add(hbox) + self._pack_noexpand_nofill(self.host_status_expander) + + def set_os_image(self, image): + self.os_image.set_from_stock(image, Gtk.IconSize.DIALOG) + + def set_vulnerability_image(self, image): + self.vulnerability_image.set_from_stock(image, Gtk.IconSize.DIALOG) + + def set_addresses(self, address): + self.address_expander.set_use_markup(True) + table, hbox = self.create_table_hbox() + self.address_expander.set_expanded(True) + + #print '>>> Address:', address + if ('ipv4' in address and + address['ipv4'] != 1): + self.info_ipv4_label.set_text(address['ipv4']) + + if ('ipv6' in address and + address['ipv6'] != 1): + self.info_ipv6_label.set_text(address['ipv6']) + + if ('mac' in address and + address['mac'] != 1): + self.info_mac_label.set_text(address['mac']) + + table.attach(self.ipv4_label, 0, 1, 0, 1) + table.attach(self.info_ipv4_label, 1, 2, 0, 1) + + table.attach(self.ipv6_label, 0, 1, 1, 2) + table.attach(self.info_ipv6_label, 1, 2, 1, 2) + + table.attach(self.mac_label, 0, 1, 2, 3) + table.attach(self.info_mac_label, 1, 2, 2, 3) + + self.address_expander.add(hbox) + self._pack_noexpand_nofill(self.address_expander) + + def set_hostnames(self, hostname): + if hostname: + self.hostnames_expander.set_use_markup(True) + self.hostnames_expander.set_expanded(True) + table, hbox = self.create_table_hbox() + + y1 = 1 + y2 = 2 + + for h in hostname: + name = h.get('hostname', na) + type = h.get('hostname_type', na) + + table.attach(HIGEntryLabel(_('Name - Type:')), 0, 1, y1, y2) + table.attach(HIGEntryLabel(name + ' - ' + type), 1, 2, y1, y2) + y1 += 1 + y2 += 1 + + self.hostnames_expander.add(hbox) + self._pack_noexpand_nofill(self.hostnames_expander) + + def set_os(self, os): + if os: + self.os_expander.set_use_markup(True) + self.os_expander.set_expanded(True) + table, hbox = self.create_table_hbox() + progress = Gtk.ProgressBar() + + if 'accuracy' in os: + progress.set_fraction(float(os['accuracy']) / 100.0) + progress.set_text(os['accuracy'] + '%') + else: + progress.set_text(_('Not Available')) + + table.attach(HIGEntryLabel(_('Name:')), 0, 1, 0, 1) + table.attach(HIGEntryLabel(os['name']), 1, 2, 0, 1) + + table.attach(HIGEntryLabel(_('Accuracy:')), 0, 1, 1, 2) + table.attach(progress, 1, 2, 1, 2) + + y1 = 2 + y2 = 3 + + if 'portsused' in os: + self.set_ports_used(os['portsused']) + table.attach(self.portsused_expander, 0, 2, y1, y2) + y1 += 1 + y2 += 1 + + if 'osclasses' in os: + self.set_osclass(os['osclasses']) + self.osclass_expander.set_use_markup(True) + table.attach(self.osclass_expander, 0, 2, y1, y2) + + self.os_expander.add(hbox) + self._pack_noexpand_nofill(self.os_expander) + + def set_ports_used(self, ports): + self.portsused_expander.set_use_markup(True) + table, hbox = self.create_table_hbox() + + y1 = 0 + y2 = 1 + + for p in ports: + table.attach(HIGEntryLabel( + _('Port-Protocol-State:')), 0, 1, y1, y2) + table.attach(HIGEntryLabel( + p['portid'] + ' - ' + p['proto'] + ' - ' + p['state'] + ), 1, 2, y1, y2) + y1 += 1 + y2 += 1 + + self.portsused_expander.add(hbox) + + def set_osclass(self, osclass): + if osclass: + self.osclass_expander.set_use_markup(True) + table, hbox = self.create_table_hbox() + + table.attach(HIGEntryLabel(_('Type')), 0, 1, 0, 1) + table.attach(HIGEntryLabel(_('Vendor')), 1, 2, 0, 1) + table.attach(HIGEntryLabel(_('OS Family')), 2, 3, 0, 1) + table.attach(HIGEntryLabel(_('OS Generation')), 3, 4, 0, 1) + table.attach(HIGEntryLabel(_('Accuracy')), 4, 5, 0, 1) + + y1 = 1 + y2 = 2 + + for o in osclass: + table.attach(HIGEntryLabel(o['type']), 0, 1, y1, y2) + table.attach(HIGEntryLabel(o['vendor']), 1, 2, y1, y2) + table.attach(HIGEntryLabel(o['osfamily']), 2, 3, y1, y2) + table.attach(HIGEntryLabel(o['osgen']), 3, 4, y1, y2) + + progress = Gtk.ProgressBar() + progress.set_text(o['accuracy'] + '%') + progress.set_fraction(float(o['accuracy']) / 100.0) + table.attach(progress, 4, 5, y1, y2) + y1 += 1 + y2 += 1 + + self.osclass_expander.add(hbox) + + def set_tcpseq(self, tcpseq): + if tcpseq: + self.tcp_expander.set_use_markup(True) + table, hbox = self.create_table_hbox() + + combo = Gtk.ComboBoxText() + for v in tcpseq['values'].split(','): + combo.append_text(v) + + table.attach(HIGEntryLabel(_('Difficulty:')), 0, 1, 1, 2) + table.attach(HIGEntryLabel(tcpseq['difficulty']), 1, 2, 1, 2) + + table.attach(HIGEntryLabel(_('Index:')), 0, 1, 2, 3) + table.attach(HIGEntryLabel(tcpseq['index']), 1, 2, 2, 3) + + table.attach(HIGEntryLabel(_('Values:')), 0, 1, 3, 4) + table.attach(combo, 1, 2, 3, 4) + + self.tcp_expander.add(hbox) + self._pack_noexpand_nofill(self.tcp_expander) + + def set_ipseq(self, ipseq): + if ipseq: + self.ip_expander.set_use_markup(True) + table, hbox = self.create_table_hbox() + + combo = Gtk.ComboBoxText() + + for i in ipseq['values'].split(','): + combo.append_text(i) + + table.attach(HIGEntryLabel(_('Class:')), 0, 1, 0, 1) + table.attach(HIGEntryLabel(ipseq['class']), 1, 2, 0, 1) + + table.attach(HIGEntryLabel(_('Values:')), 0, 1, 1, 2) + table.attach(combo, 1, 2, 1, 2) + + self.ip_expander.add(hbox) + self._pack_noexpand_nofill(self.ip_expander) + + def set_tcptsseq(self, tcptsseq): + if tcptsseq: + self.tcpts_expander.set_use_markup(True) + table, hbox = self.create_table_hbox() + + combo = Gtk.ComboBoxText() + + for i in tcptsseq['values'].split(','): + combo.append_text(i) + + table.attach(HIGEntryLabel(_('Class:')), 0, 1, 0, 1) + table.attach(HIGEntryLabel(tcptsseq['class']), 1, 2, 0, 1) + + table.attach(HIGEntryLabel(_('Values:')), 0, 1, 1, 2) + table.attach(combo, 1, 2, 1, 2) + + self.tcpts_expander.add(hbox) + self._pack_noexpand_nofill(self.tcpts_expander) + + def set_comment(self, comment=''): + self.comment_expander.set_use_markup(True) + if comment: + self.comment_expander.set_expanded(True) + + hbox = HIGHBox() + + self.comment_scrolled = Gtk.ScrolledWindow() + self.comment_scrolled.set_border_width(5) + self.comment_scrolled.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + self.comment_txt_vw = Gtk.TextView() + self.comment_txt_vw.set_wrap_mode(Gtk.WrapMode.WORD) + self.comment_txt_vw.get_buffer().set_text(comment) + + self.comment_scrolled.add(self.comment_txt_vw) + hbox._pack_expand_fill(self.comment_scrolled) + + self.comment_expander.add(hbox) + self._pack_noexpand_nofill(self.comment_expander) + + def get_comment(self): + buffer = self.comment_txt_vw.get_buffer() + return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter()) diff --git a/zenmap/zenmapGUI/ScanHostsView.py b/zenmap/zenmapGUI/ScanHostsView.py new file mode 100644 index 0000000..107cc4f --- /dev/null +++ b/zenmap/zenmapGUI/ScanHostsView.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.higwidgets.higboxes import HIGVBox +from zenmapGUI.Icons import get_os_icon +import zenmapCore.I18N # lgtm[py/unused-import] + + +def treemodel_get_addrs_for_sort(model, iter): + host = model.get_value(iter, 0) + return host.get_addrs_for_sort() + + +# Used to sort hosts by address. +def cmp_treemodel_addr(model, iter_a, iter_b, *_): + def cmp(a, b): + return (a > b) - (a < b) + addrs_a = treemodel_get_addrs_for_sort(model, iter_a) + addrs_b = treemodel_get_addrs_for_sort(model, iter_b) + return cmp(addrs_a, addrs_b) + + +class ScanHostsView(HIGVBox): + HOST_MODE, SERVICE_MODE = list(range(2)) + + def __init__(self, scan_interface): + HIGVBox.__init__(self) + + self._scan_interface = scan_interface + self._create_widgets() + self._connect_widgets() + self._pack_widgets() + self._set_scrolled() + self._set_host_list() + self._set_service_list() + + self._pack_expand_fill(self.main_vbox) + + self.mode = None + + # Default mode is host mode + self.host_mode(self.host_mode_button) + + self.host_view.show_all() + self.service_view.show_all() + + def _create_widgets(self): + # Mode buttons + self.host_mode_button = Gtk.ToggleButton.new_with_label(_("Hosts")) + self.service_mode_button = Gtk.ToggleButton.new_with_label(_("Services")) + self.buttons_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) + + # Main window vbox + self.main_vbox = HIGVBox() + + # Host list + self.host_list = Gtk.ListStore.new([object, str, str]) + self.host_list.set_sort_func(1000, cmp_treemodel_addr) + self.host_list.set_sort_column_id(1000, Gtk.SortType.ASCENDING) + self.host_view = Gtk.TreeView.new_with_model(self.host_list) + self.pic_column = Gtk.TreeViewColumn(title=_('OS')) + self.host_column = Gtk.TreeViewColumn(title=_('Host')) + self.os_cell = Gtk.CellRendererPixbuf() + self.host_cell = Gtk.CellRendererText() + + # Service list + self.service_list = Gtk.ListStore.new([str]) + self.service_list.set_sort_column_id(0, Gtk.SortType.ASCENDING) + self.service_view = Gtk.TreeView.new_with_model(self.service_list) + self.service_column = Gtk.TreeViewColumn(title=_('Service')) + self.service_cell = Gtk.CellRendererText() + + self.scrolled = Gtk.ScrolledWindow() + + def _pack_widgets(self): + self.main_vbox.set_spacing(0) + self.main_vbox.set_border_width(0) + self.main_vbox._pack_noexpand_nofill(self.buttons_box) + self.main_vbox._pack_expand_fill(self.scrolled) + + self.host_mode_button.set_active(True) + + self.buttons_box.set_border_width(5) + self.buttons_box.pack_start(self.host_mode_button, True, True, 0) + self.buttons_box.pack_start(self.service_mode_button, True, True, 0) + + def _connect_widgets(self): + self.host_mode_button.connect("toggled", self.host_mode) + self.service_mode_button.connect("toggled", self.service_mode) + + def host_mode(self, widget): + self._remove_scrolled_child() + if widget.get_active(): + self.mode = self.HOST_MODE + self.service_mode_button.set_active(False) + self.scrolled.add(self.host_view) + else: + self.service_mode_button.set_active(True) + + def service_mode(self, widget): + self._remove_scrolled_child() + if widget.get_active(): + self.mode = self.SERVICE_MODE + self.host_mode_button.set_active(False) + self.scrolled.add(self.service_view) + else: + self.host_mode_button.set_active(True) + + def _remove_scrolled_child(self): + try: + child = self.scrolled.get_child() + self.scrolled.remove(child) + except Exception: + pass + + def _set_scrolled(self): + self.scrolled.set_border_width(5) + self.scrolled.set_size_request(150, -1) + self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + def _set_service_list(self): + self.service_view.set_enable_search(True) + self.service_view.set_search_column(0) + + selection = self.service_view.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + self.service_view.append_column(self.service_column) + + self.service_column.set_resizable(True) + self.service_column.set_sort_column_id(0) + self.service_column.set_reorderable(True) + self.service_column.pack_start(self.service_cell, True) + self.service_column.set_attributes(self.service_cell, text=0) + + def _set_host_list(self): + self.host_view.set_enable_search(True) + self.host_view.set_search_column(1) + + selection = self.host_view.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + + self.host_view.append_column(self.pic_column) + self.host_view.append_column(self.host_column) + + self.host_column.set_resizable(True) + self.pic_column.set_resizable(True) + + self.host_column.set_sort_column_id(1000) + self.pic_column.set_sort_column_id(1) + + self.host_column.set_reorderable(True) + self.pic_column.set_reorderable(True) + + self.pic_column.pack_start(self.os_cell, True) + self.host_column.pack_start(self.host_cell, True) + + self.pic_column.set_min_width(35) + self.pic_column.set_attributes(self.os_cell, stock_id=1) + self.host_column.set_attributes(self.host_cell, text=2) + + def mass_update(self, hosts): + """Update the internal ListStores to reflect the hosts and services + passed in. Hosts that have not changed are left alone.""" + hosts = set(hosts) + services = set() + for h in hosts: + services.update([s["service_name"] for s in h.services]) + + # Disable sorting while elements are added. See the PyGTK FAQ 13.43, + # "Are there tips for improving performance when adding many rows to a + # Treeview?" + sort_column_id = self.host_list.get_sort_column_id() + self.host_list.set_default_sort_func(lambda *args: -1) + self.host_list.set_sort_column_id(-1, Gtk.SortType.ASCENDING) + self.host_view.freeze_child_notify() + self.host_view.set_model(None) + + it = self.host_list.get_iter_first() + # Remove any of our ListStore hosts that aren't in the list passed in. + while it: + host = self.host_list.get_value(it, 0) + if host in hosts: + hosts.remove(host) + self.host_list.set(it, 1, get_os_icon(host)) + it = self.host_list.iter_next(it) + else: + if not self.host_list.remove(it): + it = None + # Add any remaining hosts into our ListStore. + for host in hosts: + self.add_host(host) + + # Reenable sorting. + if sort_column_id != (None, None): + self.host_list.set_sort_column_id(*sort_column_id) + self.host_view.set_model(self.host_list) + self.host_view.thaw_child_notify() + + it = self.service_list.get_iter_first() + # Remove any of our ListStore services that aren't in the list passed + # in. + while it: + service_name = self.service_list.get_value(it, 0) + if service_name in services: + services.remove(service_name) + it = self.service_list.iter_next(it) + else: + if not self.service_list.remove(it): + it = None + # Add any remaining services into our ListStore. + for service_name in services: + self.add_service(service_name) + + def add_host(self, host): + self.host_list.append([host, get_os_icon(host), host.get_hostname()]) + + def add_service(self, service): + self.service_list.append([service]) + +if __name__ == "__main__": + w = Gtk.Window() + h = ScanHostsView(None) + w.add(h) + + w.connect("delete-event", lambda x, y: Gtk.main_quit()) + w.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/ScanInterface.py b/zenmap/zenmapGUI/ScanInterface.py new file mode 100644 index 0000000..40f59b9 --- /dev/null +++ b/zenmap/zenmapGUI/ScanInterface.py @@ -0,0 +1,962 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib + +import errno +import os +import time + +# Prevent loading PyXML +import xml +xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] + +import xml.sax + +from zenmapGUI.higwidgets.hignotebooks import HIGNotebook +from zenmapGUI.higwidgets.higboxes import HIGVBox +from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog +from zenmapGUI.higwidgets.higscrollers import HIGScrolledWindow + +from zenmapGUI.FilterBar import FilterBar +from zenmapGUI.ScanHostDetailsPage import ScanHostDetailsPage +from zenmapGUI.ScanToolbar import ScanCommandToolbar, ScanToolbar +from zenmapGUI.ScanHostsView import ScanHostsView +from zenmapGUI.ScanOpenPortsPage import ScanOpenPortsPage +from zenmapGUI.ScanNmapOutputPage import ScanNmapOutputPage +from zenmapGUI.ScanScanListPage import ScanScanListPage +from zenmapGUI.ScansListStore import ScansListStore +from zenmapGUI.TopologyPage import TopologyPage + +from zenmapCore.NetworkInventory import NetworkInventory,\ + FilteredNetworkInventory +from zenmapCore.NmapCommand import NmapCommand +from zenmapCore.UmitConf import CommandProfile, is_maemo +from zenmapCore.NmapParser import NmapParser +from zenmapCore.Paths import Path, get_extra_executable_search_paths +from zenmapCore.UmitLogging import log +from zenmapCore.NmapOptions import NmapOptions, split_quoted, join_quoted +import zenmapCore.I18N # lgtm[py/unused-import] + +# How often the live output view refreshes, in milliseconds. +NMAP_OUTPUT_REFRESH_INTERVAL = 1000 + + +class ScanInterface(HIGVBox): + """ScanInterface contains the scan toolbar and the scan results. Each + ScanInterface represents a single NetworkInventory as well as a set of + running scans.""" + + # The time delay between when you stop typing a filter string and filtering + # actually begins, in milliseconds. + FILTER_DELAY = 1000 + + def __init__(self): + HIGVBox.__init__(self) + + # The borders are consuming too much space on Maemo. Setting it to + # 0 pixels while on Maemo + if is_maemo(): + self.set_border_width(0) + + self.set_spacing(0) + + # True if nothing has happened here page yet, i.e., it's okay to load a + # scan from a file here. + self.empty = True + + # The most recent name the inventory on this page has been saved under. + self.saved_filename = None + + # The network inventory shown by this page. It may consist of multiple + # scans. + self.inventory = FilteredNetworkInventory() + + # The list of currently running scans (NmapCommand objects). + self.jobs = [] + + # The list of running and finished scans shown on the Nmap Output page. + self.scans_store = ScansListStore() + + self.top_box = HIGVBox() + + self.__create_toolbar() + self.__create_command_toolbar() + + self.select_default_profile() + + self.scan_result = ScanResult(self.inventory, self.scans_store, + scan_interface=self) + self.host_view_selection = self.scan_result.get_host_selection() + self.service_view_selection = self.scan_result.get_service_selection() + self.host_view_selection.connect( + 'changed', self.host_selection_changed) + self.service_view_selection.connect( + 'changed', self.service_selection_changed) + host_page = self.scan_result.scan_result_notebook.open_ports.host + host_page.host_view.get_selection().connect( + 'changed', self.service_host_selection_changed) + self.host_view_selection.connect( + 'changed', self.host_selection_changed) + + self.scan_result.scan_result_notebook.nmap_output.connect( + "changed", self._displayed_scan_change_cb) + self.scan_result.scan_result_notebook.scans_list.remove_button.connect( + "clicked", self._remove_scan_cb) + + # The hosts dict maps hostnames (as returned by HostInfo.get_hostname) + # to HostInfo objects. + self.hosts = {} + # The services dict maps service names ("http") to lists of dicts of + # the form + # {'host': <HostInfo object>, 'hostname': u'example.com', + # 'port_state': u'open', 'portid': u'22', 'protocol': u'tcp', + # 'service_conf': u'10', 'service_extrainfo': u'protocol 2.0', + # 'service_method': u'probed', 'service_name': u'ssh', + # 'service_product': u'OpenSSH', 'service_version': u'4.3'} + # In other words each dict has the same keys as an entry in + # HostInfo.ports, with the addition of "host" and "hostname" keys. + self.services = {} + + self.top_box.set_border_width(6) + self.top_box.set_spacing(5) + + self.top_box._pack_noexpand_nofill(self.toolbar) + self.top_box._pack_noexpand_nofill(self.command_toolbar) + + self._pack_noexpand_nofill(self.top_box) + self._pack_expand_fill(self.scan_result) + + self.scan_result.scan_result_notebook.scans_list.cancel_button.connect( + "clicked", self._cancel_scans_list_cb) + self.update_cancel_button() + + # Create the filter GUI + self.filter_bar = FilterBar() + self.pack_start(self.filter_bar, False, True, 0) + self.filter_bar.set_no_show_all(True) + + self.filter_timeout_id = None + + self.filter_bar.connect("changed", self.filter_changed) + self.scan_result.filter_toggle_button.connect("toggled", + self.filter_toggle_toggled) + self.scan_result.filter_toggle_button.show() + + def toggle_filter_bar(self): + self.scan_result.filter_toggle_button.clicked() + + def filter_toggle_toggled(self, widget): + if self.scan_result.filter_toggle_button.get_active(): + # Show the filter bar + self.filter_bar.show() + self.filter_bar.grab_focus() + self.filter_hosts(self.filter_bar.get_filter_string()) + else: + # Hide the filter bar + self.filter_bar.hide() + self.filter_hosts("") + + self.update_ui() + + def filter_changed(self, filter_bar): + # Restart the timer to start the filter. + if self.filter_timeout_id: + GLib.source_remove(self.filter_timeout_id) + self.filter_timeout_id = GLib.timeout_add( + self.FILTER_DELAY, self.filter_hosts, + filter_bar.get_filter_string()) + + def filter_hosts(self, filter_string): + start = time.perf_counter() + self.inventory.apply_filter(filter_string) + filter_time = time.perf_counter() - start + # Update the gui + start = time.perf_counter() + self.update_ui() + gui_time = time.perf_counter() - start + + if filter_time + gui_time > 0.0: + log.debug("apply_filter %g ms update_ui %g ms (%.0f%% filter)" % + (filter_time * 1000.0, gui_time * 1000.0, + 100.0 * filter_time / (filter_time + gui_time))) + + self.filter_timeout_id = None + return False + + def is_changed(self): + """Return true if this window has unsaved changes.""" + for scan in self.inventory.get_scans(): + if scan.unsaved: + return True + return False + changed = property(is_changed) + + def num_scans_running(self): + return len(self.jobs) + + def select_default_profile(self): + """Select a "default" profile. Currently this is defined to be the + first profile.""" + if len(self.toolbar.profile_entry.get_model()) > 0: + self.toolbar.profile_entry.set_active(0) + + def go_to_host(self, hostname): + """Scroll the text output to the appearance of the named host.""" + self.scan_result.scan_result_notebook.nmap_output.nmap_output.go_to_host(hostname) # noqa + + def __create_toolbar(self): + self.toolbar = ScanToolbar() + + self.target_entry_changed_handler = self.toolbar.target_entry.connect( + 'changed', self._target_entry_changed) + self.profile_entry_changed_handler = \ + self.toolbar.profile_entry.connect( + 'changed', self._profile_entry_changed) + + self.toolbar.scan_button.connect('clicked', self.start_scan_cb) + self.toolbar.cancel_button.connect('clicked', self._cancel_scan_cb) + + def __create_command_toolbar(self): + self.command_toolbar = ScanCommandToolbar() + self.command_toolbar.command_entry.connect( + 'activate', lambda x: self.toolbar.scan_button.clicked()) + self.command_entry_changed_handler = \ + self.command_toolbar.command_entry.connect( + 'changed', self._command_entry_changed) + + def _command_entry_changed(self, editable): + ops = NmapOptions() + ops.parse_string(self.command_toolbar.get_command()) + + # Set the target and profile without propagating the "changed" signal + # back to the command entry. + self.set_target_quiet(join_quoted(ops.target_specs)) + self.set_profile_name_quiet("") + + def _target_entry_changed(self, editable): + target_string = self.toolbar.get_selected_target() + targets = split_quoted(target_string) + + ops = NmapOptions() + ops.parse_string(self.command_toolbar.get_command()) + ops.target_specs = targets + self.set_command_quiet(ops.render_string()) + + def _profile_entry_changed(self, widget): + """Update the command based on the contents of the target and profile + entries. If the command corresponding to the current profile is not + blank, use it. Otherwise use the current contents of the command + entry.""" + profile_name = self.toolbar.get_selected_profile() + target_string = self.toolbar.get_selected_target() + + cmd_profile = CommandProfile() + command_string = cmd_profile.get_command(profile_name) + del(cmd_profile) + if command_string == "": + command_string = self.command_toolbar.get_command() + + ops = NmapOptions() + ops.parse_string(command_string) + + # Use the targets from the command entry, if there are any, otherwise + # use any targets from the profile. + targets = split_quoted(target_string) + if len(targets) > 0: + ops.target_specs = targets + else: + self.toolbar.set_selected_target(join_quoted(ops.target_specs)) + + self.set_command_quiet(ops.render_string()) + + def set_command_quiet(self, command_string): + """Set the command used by this scan interface, ignoring any further + "changed" signals.""" + self.command_toolbar.command_entry.handler_block( + self.command_entry_changed_handler) + self.command_toolbar.set_command(command_string) + self.command_toolbar.command_entry.handler_unblock( + self.command_entry_changed_handler) + + def set_target_quiet(self, target_string): + """Set the target string used by this scan interface, ignoring any + further "changed" signals.""" + self.toolbar.target_entry.handler_block( + self.target_entry_changed_handler) + self.toolbar.set_selected_target(target_string) + self.toolbar.target_entry.handler_unblock( + self.target_entry_changed_handler) + + def set_profile_name_quiet(self, profile_name): + """Set the profile name used by this scan interface, ignoring any + further "changed" signals.""" + self.toolbar.profile_entry.handler_block( + self.profile_entry_changed_handler) + self.toolbar.set_selected_profile(profile_name) + self.toolbar.profile_entry.handler_unblock( + self.profile_entry_changed_handler) + + def start_scan_cb(self, widget=None): + target = self.toolbar.selected_target + command = self.command_toolbar.command + profile = self.toolbar.selected_profile + + log.debug(">>> Start Scan:") + log.debug(">>> Target: '%s'" % target) + log.debug(">>> Profile: '%s'" % profile) + log.debug(">>> Command: '%s'" % command) + + if target != '': + try: + self.toolbar.add_new_target(target) + except IOError as e: + # We failed to save target_list.txt; treat it as read-only. + # Probably it's owned by root and this is a normal user. + log.debug(">>> Error saving %s: %s" % ( + Path.target_list, str(e))) + + if command == '': + warn_dialog = HIGAlertDialog( + message_format=_("Empty Nmap Command"), + secondary_text=_("There is no command to execute. " + "Maybe the selected/typed profile doesn't exist. " + "Please check the profile name or type the nmap " + "command you would like to execute."), + type=Gtk.MessageType.ERROR) + warn_dialog.run() + warn_dialog.destroy() + return + + self.execute_command(command, target, profile) + + def _displayed_scan_change_cb(self, widget): + self.update_cancel_button() + + def update_cancel_button(self): + """Make the Cancel button sensitive or not depending on whether the + currently displayed scan is running.""" + entry = self.scan_result.scan_result_notebook.nmap_output.get_active_entry() # noqa + if entry is None: + self.toolbar.cancel_button.set_sensitive(False) + else: + self.toolbar.cancel_button.set_sensitive(entry.running) + + def _cancel_scan_cb(self, widget): + """Cancel the scan whose output is shown.""" + entry = self.scan_result.scan_result_notebook.nmap_output.get_active_entry() # noqa + if entry is not None and entry.running: + self.cancel_scan(entry.command) + + def _cancel_scans_list_cb(self, widget): + """This is like _cancel_scan_cb, but it cancels the scans that are + currently selected in the scans list, not the one whose output is + currently shown.""" + model, selection = self.scan_result.scan_result_notebook.scans_list.scans_list.get_selection().get_selected_rows() # noqa + for path in selection: + entry = model.get_value(model.get_iter(path), 0) + if entry.running: + self.cancel_scan(entry.command) + + def _remove_scan_cb(self, widget): + model, selection = self.scan_result.scan_result_notebook.scans_list.scans_list.get_selection().get_selected_rows() # noqa + selected_refs = [] + for path in selection: + # Kill running scans and remove finished scans from the inventory. + entry = model.get_value(model.get_iter(path), 0) + if entry.running: + self.cancel_scan(entry.command) + try: + # Remove it from the inventory if present. + self.inventory.remove_scan(entry.parsed) + except ValueError: + pass + # Create TreeRowReferences because those persist while we change + # the model. + selected_refs.append(Gtk.TreeRowReference.new(model, path)) + # Delete the entries from the ScansListStore. + for ref in selected_refs: + model.remove(model.get_iter(ref.get_path())) + self.update_ui() + + def collect_umit_info(self, command, parsed): + parsed.profile_name = command.profile + parsed.nmap_command = command.command + + def kill_all_scans(self): + """Kill all running scans.""" + for scan in self.jobs: + try: + scan.kill() + except AttributeError: + pass + del self.jobs[:] + + def cancel_scan(self, command): + """Cancel a running scan.""" + self.scans_store.cancel_running_scan(command) + command.kill() + self.jobs.remove(command) + self.update_cancel_button() + + def execute_command(self, command, target=None, profile=None): + """Run the given Nmap command. Add it to the list of running scans. + Schedule a timer to refresh the output and check the scan for + completion.""" + try: + command_execution = NmapCommand(command) + except IOError as e: + warn_dialog = HIGAlertDialog( + message_format=_("Error building command"), + secondary_text=_("Error message: %s") % str(e), + type=Gtk.MessageType.ERROR) + warn_dialog.run() + warn_dialog.destroy() + return + command_execution.profile = profile + + try: + command_execution.run_scan() + except OSError as e: + text = e.strerror + # Handle ENOENT specially. + if e.errno == errno.ENOENT: + # nmap_command_path comes from zenmapCore.NmapCommand. + path_env = os.getenv("PATH") + if path_env is None: + default_paths = [] + else: + default_paths = path_env.split(os.pathsep) + text += "\n\n{}\n\n{}".format( + _("This means that the nmap executable was " + "not found in your system PATH, which is"), + path_env or _("<undefined>") + ) + extra_paths = get_extra_executable_search_paths() + extra_paths = [p for p in extra_paths if ( + p not in default_paths)] + if len(extra_paths) > 0: + if len(extra_paths) == 1: + text += "\n\n" + _("plus the extra directory") + else: + text += "\n\n" + _("plus the extra directories") + text += "\n\n" + os.pathsep.join(extra_paths) + else: + text += " (%d)" % e.errno + warn_dialog = HIGAlertDialog( + message_format=_("Error executing command"), + secondary_text=text, type=Gtk.MessageType.ERROR) + warn_dialog.run() + warn_dialog.destroy() + return + except Exception as e: + warn_dialog = HIGAlertDialog( + message_format=_("Error executing command"), + secondary_text=str(e), + type=Gtk.MessageType.ERROR) + warn_dialog.run() + warn_dialog.destroy() + return + + log.debug("Running command: %s" % command_execution.command) + self.jobs.append(command_execution) + + i = self.scans_store.add_running_scan(command_execution) + self.scan_result.scan_result_notebook.nmap_output.set_active_iter(i) + + # When scan starts, change to nmap output view tab and refresh output + self.scan_result.change_to_nmap_output_tab() + self.scan_result.refresh_nmap_output() + + # Add a timeout function + self.verify_thread_timeout_id = GLib.timeout_add( + NMAP_OUTPUT_REFRESH_INTERVAL, self.verify_execution) + + def verify_execution(self): + """This is a callback that is called periodically to refresh the output + check whether any running scans have finished. The timer that schedules + the callback is started in execute_command. When there are no more + running scans, this function returns True so that it won't be scheduled + again.""" + self.scan_result.refresh_nmap_output() + + finished_jobs = [] + for scan in self.jobs: + try: + alive = scan.scan_state() + if alive: + continue + except Exception as e: + log.debug("Scan terminated unexpectedly: %s (%s)" % (scan.command, e)) + self.scans_store.fail_running_scan(scan) + else: + log.debug("Scan finished: %s" % scan.command) + self.load_from_command(scan) + scan.close() + self.update_cancel_button() + finished_jobs.append(scan) + + # Remove finished jobs from the job list + for finished in finished_jobs: + self.jobs.remove(finished) + del(finished_jobs) + + return len(self.jobs) != 0 + + def load_from_command(self, command): + """Load scan results from a completed NmapCommand.""" + parsed = NmapParser() + try: + parsed.parse_file(command.get_xml_output_filename()) + except IOError as e: + # It's possible to run Nmap without generating an XML output file, + # like with "nmap -V". + if e.errno != errno.ENOENT: + raise + except xml.sax.SAXParseException as e: + try: + # Some options like --iflist cause Nmap to emit an empty XML + # file. Ignore the exception in this case. + st = os.stat(command.get_xml_output_filename()) + except Exception: + st = None + if st is None or st.st_size > 0: + warn_dialog = HIGAlertDialog( + message_format=_("Parse error"), + secondary_text=_( + "There was an error while parsing the XML file " + "generated from the scan:\n\n%s""") % str(e), + type=Gtk.MessageType.ERROR) + warn_dialog.run() + warn_dialog.destroy() + else: + parsed.unsaved = True + + self.scan_result.refresh_nmap_output() + try: + self.inventory.add_scan(parsed) + except Exception as e: + warn_dialog = HIGAlertDialog( + message_format=_("Cannot merge scan"), + secondary_text=_( + "There was an error while merging the new scan's " + "XML:\n\n%s") % str(e), + type=Gtk.MessageType.ERROR) + warn_dialog.run() + warn_dialog.destroy() + parsed.set_xml_is_temp(command.xml_is_temp) + self.collect_umit_info(command, parsed) + try: + parsed.nmap_output = command.get_output() + except MemoryError: + self.scan_result.scan_result_notebook.nmap_output.nmap_output.show_large_output_message(command) # noqa + self.update_ui() + self.scans_store.finish_running_scan(command, parsed) + + def load_from_file(self, filename): + """Load scan results from a saved file.""" + parsed = NmapParser() + parsed.parse(filename) + parsed.unsaved = False + + self.update_target_profile(parsed) + self.inventory.add_scan(parsed, filename=filename) + self.update_ui() + i = self.scans_store.add_scan(parsed) + log.info("scans_store.add_scan") + self.scan_result.scan_result_notebook.nmap_output.set_active_iter(i) + self.scan_result.change_to_ports_hosts_tab() + + def load_from_parsed_result(self, parsed_result): + """Load scan results from a parsed NmapParser object.""" + parsed = parsed_result + parsed.unsaved = False + + self.update_target_profile(parsed) + self.inventory.add_scan(parsed) + self.update_ui() + i = self.scans_store.add_scan(parsed) + self.scan_result.scan_result_notebook.nmap_output.set_active_iter(i) + self.scan_result.change_to_ports_hosts_tab() + + def update_target_profile(self, parsed): + """Update the "Target" and "Profile" entries based on the contents of a + parsed scan.""" + targets = parsed.get_targets() + profile_name = parsed.get_profile_name() + + self.set_command_quiet(parsed.get_nmap_command() or "") + self.set_target_quiet(join_quoted(targets)) + self.set_profile_name_quiet(profile_name or "") + + def update_ui(self): + """Update the interface's lists of hosts and ports from a parsed + scan.""" + self.empty = False + + up_hosts = self.inventory.get_hosts_up() + + self.scan_result.scan_host_view.mass_update(up_hosts) + + self.scan_result.scan_result_notebook.topology.update_radialnet() + + self.hosts = {} + self.services = {} + for host in up_hosts: + hostname = host.get_hostname() + + for service in host.services: + name = service["service_name"] + + if name not in self.services.keys(): + self.services[name] = [] + + hs = {"host": host, "hostname": hostname} + hs.update(service) + + self.services[name].append(hs) + + self.hosts[hostname] = host + + # If the host and service selection is empty or has become empty, + # select the first host if there is at least one. + if (len(self.service_view_selection.get_selected_rows()[1]) == 0 and + len(self.host_view_selection.get_selected_rows()[1]) == 0 and + len(self.scan_result.scan_host_view.host_list) > 0): + self.host_view_selection.select_iter( + self.scan_result.scan_host_view.host_list.get_iter_first()) + + self.filter_bar.set_information_text(_("%d/%d hosts shown") % + (len(self.inventory.get_hosts_up()), + len(NetworkInventory.get_hosts_up(self.inventory)))) + + mode = self.scan_result.scan_host_view.mode + if mode == ScanHostsView.HOST_MODE: + self.refresh_port_output() + elif mode == ScanHostsView.SERVICE_MODE: + self.refresh_host_output() + + def refresh_port_output(self): + """Refresh the "Ports" output of the "Ports / Hosts" tab to reflect the + current host selection.""" + self.scan_result.scan_result_notebook.port_mode() + + model_host_list, selection = \ + self.host_view_selection.get_selected_rows() + host_objs = [] + for i in selection: + hostname = model_host_list[i[0]][2] + if hostname in self.hosts: + host_objs.append(self.hosts[hostname]) + + if len(host_objs) == 1: + self.set_single_host_port(host_objs[0]) + else: + self.set_multiple_host_port(host_objs) + self.switch_host_details(self.build_host_details(host_objs)) + + def refresh_host_output(self): + """Refresh the "Hosts" output of the "Ports / Hosts" tab to reflect the + current service selection.""" + self.scan_result.scan_result_notebook.host_mode() + + model_service_list, selection = \ + self.service_view_selection.get_selected_rows() + serv_objs = [] + for i in selection: + key = model_service_list[i[0]][0] + if key in self.services: + serv_objs.append(self.services[key]) + + # Each element of serv_objs is a list of port dicts. + if len(serv_objs) == 1: + self.set_single_service_host(serv_objs[0]) + else: + servs = [] + for s in serv_objs: + servs.append({ + "service_name": s[0]["service_name"], + "ports": s}) + self.set_multiple_service_host(servs) + + def host_selection_changed(self, widget): + self.refresh_port_output() + # Switch nmap output to show first host occurrence + model, selection = self.host_view_selection.get_selected_rows() + for path in selection: + self.go_to_host(model[path][2]) + break + + def service_selection_changed(self, widget): + self.refresh_host_output() + # Change scan tab to "Ports/Hosts" + self.scan_result.change_to_ports_hosts_tab() + + def service_host_selection_changed(self, selection): + """This is the callback called when the view is in "Services" mode and + the user changes the selection among the many hosts displayed for a + given service.""" + model, selection = selection.get_selected_rows() + host_objs = [] + for path in selection: + host_objs.append(model.get_value(model.get_iter(path), 2)) + self.switch_host_details(self.build_host_details(host_objs)) + + def switch_host_details(self, pages): + """Switch the "Host Details" view to show the ScanHostDetailsPages in + the given list.""" + vbox = self.scan_result.scan_result_notebook.host_details_vbox + + # Remove the old children. + for child in vbox.get_children(): + vbox.remove(child) + + for p in pages: + p.set_expanded(False) + vbox._pack_noexpand_nofill(p) + if len(pages) == 1: + pages[0].set_expanded(True) + vbox.show_all() + + def _save_comment(self, widget, extra, host): + """Sets the comment on a host from the contents of the comment text + entry.""" + buff = widget.get_buffer() + comment = buff.get_text( + buff.get_start_iter(), buff.get_end_iter()) + if host.comment == comment: + # no change, ignore + return + host.comment = comment + for scan in self.inventory.get_scans(): + for h in scan.get_hosts(): + if (h.get_ip() == host.get_ip() and + h.get_ipv6() == host.get_ipv6()): + h.set_comment(host.comment) + scan.unsaved = True + break + + def build_host_details(self, hosts): + """Builds and returns a list of "Host Details" pages corresponding to + the given hosts.""" + pages = [] + for host in hosts: + page = ScanHostDetailsPage(host) + page.host_details.comment_txt_vw.connect( + "insert-at-cursor", self._save_comment, host) + page.host_details.comment_txt_vw.connect( + "focus-out-event", self._save_comment, host) + pages.append(page) + return pages + + def set_single_host_port(self, host): + """Change the "Ports / Hosts" tab to show the port output from the + single given host.""" + host_page = self.scan_result.scan_result_notebook.open_ports.host + host_page.switch_port_to_list_store() + + host_page.freeze() + host_page.clear_port_list() + for p in host.ports: + host_page.add_to_port_list(p) + host_page.thaw() + + def set_single_service_host(self, service): + """Change the "Ports / Hosts" tab to show the hosts associated with the + single named service.""" + host_page = self.scan_result.scan_result_notebook.open_ports.host + host_page.switch_host_to_list_store() + + host_page.freeze() + host_page.clear_host_list() + for p in service: + host_page.add_to_host_list(p["host"], p) + host_page.thaw() + + def set_multiple_host_port(self, host_list): + """Change the "Ports / Hosts" tab to show the port output for all of + the hosts in host_list. When multiple hosts are selected, the port + output for each is contained in an expander.""" + host_page = self.scan_result.scan_result_notebook.open_ports.host + host_page.switch_port_to_tree_store() + + host_page.freeze() + host_page.clear_port_tree() + for host in host_list: + host_page.add_to_port_tree(host) + host_page.thaw() + + def set_multiple_service_host(self, service_list): + """Change the "Ports / Hosts" tab to show the hosts associated with + each of the services in service_list. Each element of service_list must + be a dict with the keys "service_name" and "ports". When multiple + services are selected, the hosts for each are contained in an + expander.""" + host_page = self.scan_result.scan_result_notebook.open_ports.host + host_page.switch_host_to_tree_store() + + host_page.freeze() + host_page.clear_host_tree() + for service in service_list: + host_page.add_to_host_tree( + service["service_name"], service["ports"]) + host_page.thaw() + + +class ScanResult(Gtk.Paned): + """This is the pane that has the "Host"/"Service" column (ScanHostsView) on + the left and the "Nmap Output"/"Ports / Hosts"/etc. (ScanResultNotebook) on + the right. It's the part of the interface below the toolbar.""" + def __init__(self, inventory, scans_store, scan_interface=None): + Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + + self.scan_host_view = ScanHostsView(scan_interface) + self.scan_result_notebook = ScanResultNotebook(inventory, scans_store) + self.filter_toggle_button = Gtk.ToggleButton.new_with_label(_("Filter Hosts")) + + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + vbox.pack_start(self.scan_host_view, True, True, 0) + vbox.pack_start(self.filter_toggle_button, False, True, 0) + self.pack1(vbox) + self.pack2(self.scan_result_notebook, True, False) + + def set_nmap_output(self, msg): + self.scan_result_notebook.nmap_output.nmap_output.text_view.get_buffer().set_text(msg) # noqa + + def clear_nmap_output(self): + self.scan_result_notebook.nmap_output.nmap_output.text_view.get_buffer().set_text("") # noqa + + def get_host_selection(self): + return self.scan_host_view.host_view.get_selection() + + def get_service_selection(self): + return self.scan_host_view.service_view.get_selection() + + def get_nmap_output(self): + return self.scan_result_notebook.nmap_output.get_nmap_output() + + def clear_port_list(self): + self.scan_result_notebook.open_ports.host.clear_port_list() + + def change_to_ports_hosts_tab(self): + self.scan_result_notebook.set_current_page(1) + + def change_to_nmap_output_tab(self): + self.scan_result_notebook.set_current_page(0) + + def refresh_nmap_output(self): + """Refresh the Nmap output with the newest output of command_execution, + if it is not None.""" + self.scan_result_notebook.nmap_output.nmap_output.refresh_output() + + +class ScanResultNotebook(HIGNotebook): + """This is the right side of a ScanResult, the notebook with the tabs such + as "Nmap Output".""" + def __init__(self, inventory, scans_store): + HIGNotebook.__init__(self) + self.set_border_width(5) + + self.__create_widgets(inventory, scans_store) + + self.scans_list.scans_list.connect( + "row-activated", self._scan_row_activated) + + self.append_page(self.nmap_output_page, Gtk.Label.new(_('Nmap Output'))) + self.append_page(self.open_ports_page, Gtk.Label.new(_('Ports / Hosts'))) + self.append_page(self.topology_page, Gtk.Label.new(_('Topology'))) + self.append_page(self.host_details_page, Gtk.Label.new(_('Host Details'))) + self.append_page(self.scans_list_page, Gtk.Label.new(_('Scans'))) + + def host_mode(self): + self.open_ports.host.host_mode() + + def port_mode(self): + self.open_ports.host.port_mode() + + def __create_widgets(self, inventory, scans_store): + self.open_ports_page = HIGVBox() + self.nmap_output_page = HIGVBox() + self.topology_page = HIGVBox() + self.host_details_page = HIGScrolledWindow() + self.host_details_vbox = HIGVBox() + self.scans_list_page = HIGVBox() + + self.open_ports = ScanOpenPortsPage() + self.nmap_output = ScanNmapOutputPage(scans_store) + self.topology = TopologyPage(inventory) + self.scans_list = ScanScanListPage(scans_store) + + self.no_selected = Gtk.Label.new(_('No host selected.')) + self.host_details = self.no_selected + + self.open_ports_page.add(self.open_ports) + self.nmap_output_page.add(self.nmap_output) + self.topology_page.add(self.topology) + self.scans_list_page.add(self.scans_list) + + self.host_details_page.add_with_viewport(self.host_details_vbox) + self.host_details_vbox._pack_expand_fill(self.host_details) + + def _scan_row_activated(self, treeview, path, view_column): + """Switch back to the Nmap Output view when a scan is activated + (double-clicked) on the scans list.""" + self.nmap_output.set_active_iter(treeview.get_model().get_iter(path)) + self.set_current_page(0) diff --git a/zenmap/zenmapGUI/ScanNmapOutputPage.py b/zenmap/zenmapGUI/ScanNmapOutputPage.py new file mode 100644 index 0000000..a3ffa45 --- /dev/null +++ b/zenmap/zenmapGUI/ScanNmapOutputPage.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GObject, GdkPixbuf, Pango + +import os + +from zenmapGUI.higwidgets.higboxes import HIGHBox, HIGVBox + +from zenmapGUI.NmapOutputViewer import NmapOutputViewer +from zenmapGUI.ScanRunDetailsPage import ScanRunDetailsPage +from zenmapCore.Paths import Path +from zenmapCore.UmitLogging import log +import zenmapCore.I18N # lgtm[py/unused-import] + + +def scan_entry_data_func(widget, cell_renderer, model, iter): + """Set the properties of a cell renderer for a scan entry.""" + cell_renderer.set_property("ellipsize", Pango.EllipsizeMode.END) + cell_renderer.set_property("style", Pango.Style.NORMAL) + cell_renderer.set_property("strikethrough", False) + entry = model.get_value(iter, 0) + if entry is None: + return + if entry.running: + cell_renderer.set_property("style", Pango.Style.ITALIC) + elif entry.finished: + pass + elif entry.failed or entry.canceled: + cell_renderer.set_property("strikethrough", True) + cell_renderer.set_property("text", entry.get_command_string()) + + +class Throbber(Gtk.Image): + """This is a little progress indicator that animates while a scan is + running.""" + try: + still = GdkPixbuf.Pixbuf.new_from_file( + os.path.join(Path.pixmaps_dir, "throbber.png")) + anim = GdkPixbuf.PixbufAnimation( + os.path.join(Path.pixmaps_dir, "throbber.gif")) + except Exception as e: + log.debug("Error loading throbber images: %s." % str(e)) + still = None + anim = None + + def __init__(self): + Gtk.Image.__init__(self) + self.set_from_pixbuf(self.still) + self.animating = False + + def go(self): + # Don't change anything if we're already animating. + if not self.animating and self.anim is not None: + self.set_from_animation(self.anim) + self.animating = True + + def stop(self): + if self.animating and self.still is not None: + self.set_from_pixbuf(self.still) + self.animating = False + + +class ScanNmapOutputPage(HIGVBox): + """This is the "Nmap Output" scan results tab. It holds a text view of Nmap + output. The constructor takes a ScansListStore, the contents of which are + made selectable through a combo box. Details for completed scans are + available and shown in separate windows. It emits the "changed" signal when + the combo box selection changes.""" + + __gsignals__ = { + "changed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()) + } + + def __init__(self, scans_store): + HIGVBox.__init__(self) + + # This is a cache of details windows we have open. + self._details_windows = {} + + self.set_spacing(0) + + hbox = HIGHBox() + + self.scans_list = Gtk.ComboBox.new_with_model(scans_store) + cell = Gtk.CellRendererText() + self.scans_list.pack_start(cell, True) + self.scans_list.set_cell_data_func(cell, scan_entry_data_func) + hbox._pack_expand_fill(self.scans_list) + + self.scans_list.connect("changed", self._selection_changed) + scans_store.connect("row-changed", self._row_changed) + scans_store.connect("row-deleted", self._row_deleted) + + self.throbber = Throbber() + hbox._pack_noexpand_nofill(self.throbber) + + self.details_button = Gtk.Button.new_with_label(_("Details")) + self.details_button.connect("clicked", self._show_details) + hbox._pack_noexpand_nofill(self.details_button) + + self._pack_noexpand_nofill(hbox) + + self.nmap_output = NmapOutputViewer() + self._pack_expand_fill(self.nmap_output) + + self._update() + + def set_active_iter(self, i): + """Set the active entry to an iterator into the ScansListStore + referred to by this object.""" + self.scans_list.set_active_iter(i) + + def get_active_entry(self): + iter = self.scans_list.get_active_iter() + if iter is None: + return None + return self.scans_list.get_model().get_value(iter, 0) + + def _selection_changed(self, widget): + """This callback is called when a scan in the list of scans is + selected.""" + self._update() + self.emit("changed") + + def _row_changed(self, model, path, i): + """This callback is called when a row in the underlying scans store is + changed.""" + # If the currently selected entry was changed, update the interface. + if path[0] == self.scans_list.get_active(): + self._update() + + def _row_deleted(self, model, path): + """This callback is called when a row in the underlying scans store is + deleted.""" + self._update() + + def _update(self): + """Update the interface based on the currently selected entry.""" + entry = self.get_active_entry() + if entry is None: + self.nmap_output.show_nmap_output("") + self.details_button.set_sensitive(False) + self.throbber.stop() + return + + if entry.parsed is not None: + self.nmap_output.set_command_execution(None) + nmap_output = entry.parsed.get_nmap_output() + if nmap_output: + self.nmap_output.show_nmap_output(nmap_output) + self.details_button.set_sensitive(True) + elif entry.command is not None: + self.nmap_output.set_command_execution(entry.command) + self.nmap_output.refresh_output() + self.details_button.set_sensitive(False) + + if entry.running: + self.throbber.go() + else: + self.throbber.stop() + + def _show_details(self, button): + """Show a details window for the currently selected scan, if it is + finished.""" + entry = self.get_active_entry() + if entry is None: + return + if not entry.finished: + return + if self._details_windows.get(entry) is None: + window = Gtk.Window() + window.add(ScanRunDetailsPage(entry.parsed)) + + def close_details(details, event, entry): + details.destroy() + del self._details_windows[entry] + + window.connect("delete-event", close_details, entry) + window.show_all() + self._details_windows[entry] = window + self._details_windows[entry].present() diff --git a/zenmap/zenmapGUI/ScanOpenPortsPage.py b/zenmap/zenmapGUI/ScanOpenPortsPage.py new file mode 100644 index 0000000..a52a3f7 --- /dev/null +++ b/zenmap/zenmapGUI/ScanOpenPortsPage.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.higwidgets.higboxes import HIGVBox + +from zenmapCore.UmitLogging import log +import zenmapCore.I18N # lgtm[py/unused-import] + + +def findout_service_icon(port_info): + if port_info["port_state"] in ["open", "open|filtered"]: + return Gtk.STOCK_YES + else: + return Gtk.STOCK_NO + + +def get_version_string(d): + """Get a human-readable version string from the dict d. The keys used in d + are "service_product", "service_version", and "service_extrainfo" (all are + optional). This produces a string like "OpenSSH 4.3p2 Debian 9etch2 + (protocol 2.0)".""" + result = [] + if d.get("service_product"): + result.append(d["service_product"]) + if d.get("service_version"): + result.append(d["service_version"]) + if d.get("service_extrainfo"): + result.append("(" + d["service_extrainfo"] + ")") + return " ".join(result) + + +def get_addrs(host): + if host is None: + return [] + return host.get_addrs_for_sort() + + +def cmp_addrs(host_a, host_b): + def cmp(a, b): + return (a > b) - (a < b) + return cmp(get_addrs(host_a), get_addrs(host_b)) + + +def cmp_port_list_addr(model, iter_a, iter_b, *_): + host_a = model.get_value(iter_a, 0) + host_b = model.get_value(iter_b, 0) + return cmp_addrs(host_a, host_b) + + +def cmp_port_tree_addr(model, iter_a, iter_b, *_): + host_a = model.get_value(iter_a, 0) + host_b = model.get_value(iter_b, 0) + return cmp_addrs(host_a, host_b) + + +def cmp_host_list_addr(model, iter_a, iter_b, *_): + host_a = model.get_value(iter_a, 2) + host_b = model.get_value(iter_b, 2) + return cmp_addrs(host_a, host_b) + + +def cmp_host_tree_addr(model, iter_a, iter_b, *_): + host_a = model.get_value(iter_a, 2) + host_b = model.get_value(iter_b, 2) + return cmp_addrs(host_a, host_b) + + +class ScanOpenPortsPage(Gtk.ScrolledWindow): + def __init__(self): + Gtk.ScrolledWindow.__init__(self) + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + self.__create_widgets() + + self.add_with_viewport(self.host) + + def __create_widgets(self): + self.host = HostOpenPorts() + + +class HostOpenPorts(HIGVBox): + def __init__(self): + HIGVBox.__init__(self) + + self._create_widgets() + self._set_port_list() + self._set_host_list() + self._pack_widgets() + + def _create_widgets(self): + # Ports view + self.port_columns = {} + # host hostname icon port protocol state service version + # The hostname column is shown only when more than one host is selected + # (hence port_tree not port_list is used). + self.port_list = Gtk.ListStore.new([ + object, str, str, int, str, str, str, str]) + self.port_tree = Gtk.TreeStore.new([ + object, str, str, int, str, str, str, str]) + + self.port_list.set_sort_func(1000, cmp_port_list_addr) + self.port_list.set_sort_column_id(1000, Gtk.SortType.ASCENDING) + self.port_tree.set_sort_func(1000, cmp_port_tree_addr) + self.port_tree.set_sort_column_id(1000, Gtk.SortType.ASCENDING) + + self.port_view = Gtk.TreeView.new_with_model(self.port_list) + + self.cell_icon = Gtk.CellRendererPixbuf() + self.cell_port = Gtk.CellRendererText() + + self.port_columns['hostname'] = Gtk.TreeViewColumn(title=_('Host')) + self.port_columns['icon'] = Gtk.TreeViewColumn(title='') + self.port_columns['port_number'] = Gtk.TreeViewColumn(title=_('Port')) + self.port_columns['protocol'] = Gtk.TreeViewColumn(title=_('Protocol')) + self.port_columns['state'] = Gtk.TreeViewColumn(title=_('State')) + self.port_columns['service'] = Gtk.TreeViewColumn(title=_('Service')) + self.port_columns['version'] = Gtk.TreeViewColumn(title=_('Version')) + + # Host services view + self.host_columns = {} + # service icon host hostname port protocol state version + # service is shown only when more than one service is selected (hence + # host_tree not host_list is used). + self.host_list = Gtk.ListStore.new([ + str, str, object, str, int, str, str, str]) + self.host_tree = Gtk.TreeStore.new([ + str, str, object, str, int, str, str, str]) + + self.host_list.set_sort_func(1000, cmp_host_list_addr) + self.host_list.set_sort_column_id(1000, Gtk.SortType.ASCENDING) + self.host_tree.set_sort_func(1000, cmp_host_tree_addr) + self.host_tree.set_sort_column_id(1000, Gtk.SortType.ASCENDING) + + self.host_view = Gtk.TreeView.new_with_model(self.host_list) + + self.cell_host_icon = Gtk.CellRendererPixbuf() + self.cell_host = Gtk.CellRendererText() + + self.host_columns['service'] = Gtk.TreeViewColumn(title=_('Service')) + self.host_columns['icon'] = Gtk.TreeViewColumn(title='') + self.host_columns['hostname'] = Gtk.TreeViewColumn(title=_('Hostname')) + self.host_columns['protocol'] = Gtk.TreeViewColumn(title=_('Protocol')) + self.host_columns['port_number'] = Gtk.TreeViewColumn(title=_('Port')) + self.host_columns['state'] = Gtk.TreeViewColumn(title=_('State')) + self.host_columns['version'] = Gtk.TreeViewColumn(title=_('Version')) + + self.scroll_ports_hosts = Gtk.ScrolledWindow() + + def _set_host_list(self): + self.host_view.set_enable_search(True) + self.host_view.set_search_column(2) + + selection = self.host_view.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + + columns = ["service", "icon", "hostname", "port_number", + "protocol", "state", "version"] + + for c in columns: + self.host_view.append_column(self.host_columns[c]) + self.host_columns[c].set_reorderable(True) + self.host_columns[c].set_resizable(True) + + self.host_columns['service'].set_sort_column_id(0) + self.host_columns['icon'].set_min_width(35) + self.host_columns['icon'].set_sort_column_id(6) + self.host_columns['hostname'].set_sort_column_id(1000) + self.host_columns['port_number'].set_sort_column_id(4) + self.host_columns['protocol'].set_sort_column_id(5) + self.host_columns['state'].set_sort_column_id(6) + self.host_columns['version'].set_sort_column_id(7) + + self.host_columns['service'].pack_start(self.cell_port, True) + self.host_columns['icon'].pack_start(self.cell_host_icon, True) + self.host_columns['hostname'].pack_start(self.cell_port, True) + self.host_columns['port_number'].pack_start(self.cell_port, True) + self.host_columns['protocol'].pack_start(self.cell_port, True) + self.host_columns['version'].pack_start(self.cell_port, True) + self.host_columns['state'].pack_start(self.cell_port, True) + + self.host_columns['service'].set_attributes(self.cell_port, text=0) + self.host_columns['icon'].set_attributes( + self.cell_host_icon, stock_id=1) + self.host_columns['hostname'].set_attributes(self.cell_port, text=3) + self.host_columns['port_number'].set_attributes(self.cell_port, text=4) + self.host_columns['protocol'].set_attributes(self.cell_port, text=5) + self.host_columns['state'].set_attributes(self.cell_port, text=6) + self.host_columns['version'].set_attributes(self.cell_port, text=7) + + self.host_columns['service'].set_visible(False) + + self.scroll_ports_hosts.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + def _set_port_list(self): + self.port_view.set_enable_search(True) + self.port_view.set_search_column(3) + + selection = self.port_view.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + + self.port_view.append_column(self.port_columns['hostname']) + self.port_view.append_column(self.port_columns['icon']) + self.port_view.append_column(self.port_columns['port_number']) + self.port_view.append_column(self.port_columns['protocol']) + self.port_view.append_column(self.port_columns['state']) + self.port_view.append_column(self.port_columns['service']) + self.port_view.append_column(self.port_columns['version']) + + for k in self.port_columns: + self.port_columns[k].set_reorderable(True) + self.port_columns[k].set_resizable(True) + + self.port_columns['icon'].set_min_width(35) + + self.port_columns['hostname'].set_sort_column_id(1000) + self.port_columns['icon'].set_sort_column_id(5) + self.port_columns['port_number'].set_sort_column_id(3) + self.port_columns['protocol'].set_sort_column_id(4) + self.port_columns['state'].set_sort_column_id(5) + self.port_columns['service'].set_sort_column_id(6) + self.port_columns['version'].set_sort_column_id(7) + + self.port_columns['hostname'].pack_start(self.cell_port, True) + self.port_columns['icon'].pack_start(self.cell_icon, True) + self.port_columns['port_number'].pack_start(self.cell_port, True) + self.port_columns['protocol'].pack_start(self.cell_port, True) + self.port_columns['service'].pack_start(self.cell_port, True) + self.port_columns['version'].pack_start(self.cell_port, True) + self.port_columns['state'].pack_start(self.cell_port, True) + + self.port_columns['hostname'].set_attributes(self.cell_port, text=1) + self.port_columns['icon'].set_attributes(self.cell_icon, stock_id=2) + self.port_columns['port_number'].set_attributes(self.cell_port, text=3) + self.port_columns['protocol'].set_attributes(self.cell_port, text=4) + self.port_columns['state'].set_attributes(self.cell_port, text=5) + self.port_columns['service'].set_attributes(self.cell_port, text=6) + self.port_columns['version'].set_attributes(self.cell_port, text=7) + + self.port_columns['hostname'].set_visible(False) + + self.scroll_ports_hosts.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + def port_mode(self): + child = self.scroll_ports_hosts.get_child() + if id(child) != id(self.port_view): + if child is not None: + self.scroll_ports_hosts.remove(child) + self.scroll_ports_hosts.add(self.port_view) + self.port_view.show_all() + self.host_view.hide() + + def host_mode(self): + child = self.scroll_ports_hosts.get_child() + if id(child) != id(self.host_view): + if child is not None: + self.scroll_ports_hosts.remove(child) + self.scroll_ports_hosts.add(self.host_view) + self.host_view.show_all() + self.port_view.hide() + + def freeze(self): + """Freeze notifications and sorting to make adding lots of elements to + the model faster.""" + self.frozen_host_list_sort_column_id = \ + self.host_list.get_sort_column_id() + self.frozen_host_tree_sort_column_id = \ + self.host_tree.get_sort_column_id() + self.frozen_port_list_sort_column_id = \ + self.port_list.get_sort_column_id() + self.frozen_port_tree_sort_column_id = \ + self.port_tree.get_sort_column_id() + self.host_list.set_default_sort_func(lambda *args: -1) + self.host_tree.set_default_sort_func(lambda *args: -1) + self.port_list.set_default_sort_func(lambda *args: -1) + self.port_tree.set_default_sort_func(lambda *args: -1) + self.frozen_host_view_model = self.host_view.get_model() + self.frozen_port_view_model = self.port_view.get_model() + self.host_view.freeze_child_notify() + self.port_view.freeze_child_notify() + self.host_view.set_model(None) + self.port_view.set_model(None) + + def thaw(self): + """Restore notifications and sorting (after making changes to the + model).""" + if self.frozen_host_list_sort_column_id != (None, None): + self.host_list.set_sort_column_id( + *self.frozen_host_list_sort_column_id) + if self.frozen_host_tree_sort_column_id != (None, None): + self.host_tree.set_sort_column_id( + *self.frozen_host_tree_sort_column_id) + if self.frozen_port_list_sort_column_id != (None, None): + self.port_list.set_sort_column_id( + *self.frozen_port_list_sort_column_id) + if self.frozen_port_tree_sort_column_id != (None, None): + self.port_tree.set_sort_column_id( + *self.frozen_port_tree_sort_column_id) + self.host_view.set_model(self.frozen_host_view_model) + self.port_view.set_model(self.frozen_port_view_model) + self.host_view.thaw_child_notify() + self.port_view.thaw_child_notify() + + def add_to_port_list(self, p): + entry = [None, "", findout_service_icon(p), int(p.get('portid', '0')), + p.get('protocol', ''), p.get('port_state', ''), + p.get('service_name', ''), get_version_string(p)] + log.debug(">>> Add Port: %s" % entry) + self.port_list.append(entry) + + def add_to_host_list(self, host, p): + entry = ["", findout_service_icon(p), host, host.get_hostname(), + int(p.get('portid', '0')), p.get('protocol', ''), + p.get('port_state', ''), get_version_string(p)] + log.debug(">>> Add Host: %s" % entry) + self.host_list.append(entry) + + def add_to_port_tree(self, host): + parent = self.port_tree.append( + None, [host, host.get_hostname(), None, 0, '', '', '', '']) + for p in host.get_ports(): + self.port_tree.append(parent, + [None, '', findout_service_icon(p), int(p.get('portid', "0")), + p.get('protocol', ''), p.get('port_state', ""), + p.get('service_name', _("Unknown")), get_version_string(p)]) + + def add_to_host_tree(self, service_name, ports): + parent = self.host_tree.append( + None, [service_name, '', None, '', 0, '', '', '']) + for p in ports: + self.host_tree.append(parent, + [ + '', + findout_service_icon(p), + p["host"], + p["host"].get_hostname(), + int(p.get('portid', "0")), + p.get('protocol', ""), + p.get('port_state', _("unknown")), + get_version_string(p) + ] + ) + + def switch_port_to_list_store(self): + if self.port_view.get_model() != self.port_list: + self.port_view.set_model(self.port_list) + self.port_columns['hostname'].set_visible(False) + + def switch_port_to_tree_store(self): + if self.port_view.get_model() != self.port_tree: + self.port_view.set_model(self.port_tree) + self.port_columns['hostname'].set_visible(True) + + def switch_host_to_list_store(self): + if self.host_view.get_model() != self.host_list: + self.host_view.set_model(self.host_list) + self.host_columns['service'].set_visible(False) + + def switch_host_to_tree_store(self): + if self.host_view.get_model() != self.host_tree: + self.host_view.set_model(self.host_tree) + self.host_columns['service'].set_visible(True) + + def _pack_widgets(self): + self.scroll_ports_hosts.add(self.port_view) + self._pack_expand_fill(self.scroll_ports_hosts) + + def clear_port_list(self): + for i in range(len(self.port_list)): + iter = self.port_list.get_iter_first() + del(self.port_list[iter]) + + def clear_host_list(self): + for i in range(len(self.host_list)): + iter = self.host_list.get_iter_first() + del(self.host_list[iter]) + + def clear_port_tree(self): + for i in range(len(self.port_tree)): + iter = self.port_tree.get_iter_first() + del(self.port_tree[iter]) + + def clear_host_tree(self): + for i in range(len(self.host_tree)): + iter = self.host_tree.get_iter_first() + del(self.host_tree[iter]) + +if __name__ == "__main__": + w = Gtk.Window() + h = HostOpenPorts() + w.add(h) + + w.connect("delete-event", lambda x, y: Gtk.main_quit()) + w.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/ScanRunDetailsPage.py b/zenmap/zenmapGUI/ScanRunDetailsPage.py new file mode 100644 index 0000000..57ed419 --- /dev/null +++ b/zenmap/zenmapGUI/ScanRunDetailsPage.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.higwidgets.higboxes import HIGVBox, HIGHBox,\ + hig_box_space_holder +from zenmapGUI.higwidgets.higtables import HIGTable +from zenmapGUI.higwidgets.higlabels import HIGEntryLabel + +import zenmapCore.I18N # lgtm[py/unused-import] + + +class ScanRunDetailsPage(HIGVBox): + def __init__(self, scan): + HIGVBox.__init__(self) + + na = _('Not available') + + # Command info + self.command_label = HIGEntryLabel(_('Command:')) + self.info_command_label = HIGEntryLabel(na) + + self.nmap_version_label = HIGEntryLabel(_('Nmap Version:')) + self.info_nmap_version_label = HIGEntryLabel(na) + + self.verbose_label = HIGEntryLabel(_('Verbosity level:')) + self.info_verbose_label = HIGEntryLabel(na) + + self.debug_label = HIGEntryLabel(_('Debug level:')) + self.info_debug_label = HIGEntryLabel(na) + + self.command_expander = Gtk.Expander.new( + "<b>" + _("Command Info") + "</b>") + self.command_expander.set_use_markup(True) + + self.command_table = HIGTable() + self.command_table.set_border_width(5) + self.command_table.set_row_spacings(6) + self.command_table.set_col_spacings(6) + + self.command_hbox = HIGHBox() + self.command_hbox._pack_noexpand_nofill(hig_box_space_holder()) + self.command_hbox._pack_noexpand_nofill(self.command_table) + + self.command_table.attach(self.command_label, 0, 1, 0, 1) + self.command_table.attach(self.info_command_label, 1, 2, 0, 1) + + self.command_table.attach(self.nmap_version_label, 0, 1, 1, 2) + self.command_table.attach(self.info_nmap_version_label, 1, 2, 1, 2) + + self.command_table.attach(self.verbose_label, 0, 1, 2, 3) + self.command_table.attach(self.info_verbose_label, 1, 2, 2, 3) + + self.command_table.attach(self.debug_label, 0, 1, 3, 4) + self.command_table.attach(self.info_debug_label, 1, 2, 3, 4) + + self.command_expander.add(self.command_hbox) + self._pack_noexpand_nofill(self.command_expander) + self.command_expander.set_expanded(True) + + # General info: + self.start_label = HIGEntryLabel(_('Started on:')) + self.info_start_label = HIGEntryLabel(na) + + self.finished_label = HIGEntryLabel(_('Finished on:')) + self.info_finished_label = HIGEntryLabel(na) + + self.host_up_label = HIGEntryLabel(_('Hosts up:')) + self.info_hosts_up_label = HIGEntryLabel(na) + + self.host_down_label = HIGEntryLabel(_('Hosts down:')) + self.info_hosts_down_label = HIGEntryLabel(na) + + self.host_scanned_label = HIGEntryLabel(_('Hosts scanned:')) + self.info_hosts_scanned_label = HIGEntryLabel(na) + + self.open_label = HIGEntryLabel(_('Open ports:')) + self.info_open_label = HIGEntryLabel(na) + + self.filtered_label = HIGEntryLabel(_('Filtered ports:')) + self.info_filtered_label = HIGEntryLabel(na) + + self.closed_label = HIGEntryLabel(_('Closed ports:')) + self.info_closed_label = HIGEntryLabel(na) + + self.general_expander = Gtk.Expander.new( + "<b>" + _("General Info") + "</b>") + self.general_expander.set_use_markup(True) + + self.general_table = HIGTable() + self.general_table.set_border_width(5) + self.general_table.set_row_spacings(6) + self.general_table.set_col_spacings(6) + + self.general_hbox = HIGHBox() + self.general_hbox._pack_noexpand_nofill(hig_box_space_holder()) + self.general_hbox._pack_noexpand_nofill(self.general_table) + + self.general_table.attach(self.start_label, 0, 1, 0, 1) + self.general_table.attach(self.info_start_label, 1, 2, 0, 1) + + self.general_table.attach(self.finished_label, 0, 1, 1, 2) + self.general_table.attach(self.info_finished_label, 1, 2, 1, 2) + + self.general_table.attach(self.host_up_label, 0, 1, 2, 3) + self.general_table.attach(self.info_hosts_up_label, 1, 2, 2, 3) + + self.general_table.attach(self.host_down_label, 0, 1, 3, 4) + self.general_table.attach(self.info_hosts_down_label, 1, 2, 3, 4) + + self.general_table.attach(self.host_scanned_label, 0, 1, 4, 5) + self.general_table.attach(self.info_hosts_scanned_label, 1, 2, 4, 5) + + self.general_table.attach(self.open_label, 0, 1, 5, 6) + self.general_table.attach(self.info_open_label, 1, 2, 5, 6) + + self.general_table.attach(self.filtered_label, 0, 1, 6, 7) + self.general_table.attach(self.info_filtered_label, 1, 2, 6, 7) + + self.general_table.attach(self.closed_label, 0, 1, 7, 8) + self.general_table.attach(self.info_closed_label, 1, 2, 7, 8) + + self.general_expander.add(self.general_hbox) + self._pack_noexpand_nofill(self.general_expander) + self.general_expander.set_expanded(True) + + self._set_from_scan(scan) + + def _set_from_scan(self, scan): + """Initialize the display from a parsed scan.""" + # Command info. + self.info_command_label.set_text(scan.get_nmap_command()) + self.info_nmap_version_label.set_text(scan.get_scanner_version()) + self.info_verbose_label.set_text(scan.get_verbose_level()) + self.info_debug_label.set_text(scan.get_debugging_level()) + + # General info. + self.info_start_label.set_text(scan.get_formatted_date()) + self.info_finished_label.set_text(scan.get_formatted_finish_date()) + self.info_hosts_up_label.set_text(str(scan.get_hosts_up())) + self.info_hosts_down_label.set_text(str(scan.get_hosts_down())) + self.info_hosts_scanned_label.set_text(str(scan.get_hosts_scanned())) + self.info_open_label.set_text(str(scan.get_open_ports())) + self.info_filtered_label.set_text(str(scan.get_filtered_ports())) + self.info_closed_label.set_text(str(scan.get_closed_ports())) + + for scaninfo in scan.get_scaninfo(): + exp = Gtk.Expander.new('<b>%s - %s</b>' % ( + _('Scan Info'), scaninfo['type'].capitalize())) + exp.set_use_markup(True) + + display = self.make_scaninfo_display(scaninfo) + + exp.add(display) + self._pack_noexpand_nofill(exp) + + def make_scaninfo_display(self, scaninfo): + """Return a widget displaying a scan's "scaninfo" information: type, + protocol, number of scanned ports, and list of services.""" + hbox = HIGHBox() + table = HIGTable() + table.set_border_width(5) + table.set_row_spacings(6) + table.set_col_spacings(6) + + table.attach(HIGEntryLabel(_('Scan type:')), 0, 1, 0, 1) + table.attach(HIGEntryLabel(scaninfo['type']), 1, 2, 0, 1) + + table.attach(HIGEntryLabel(_('Protocol:')), 0, 1, 1, 2) + table.attach(HIGEntryLabel(scaninfo['protocol']), 1, 2, 1, 2) + + table.attach(HIGEntryLabel(_('# scanned ports:')), 0, 1, 2, 3) + table.attach(HIGEntryLabel(scaninfo['numservices']), 1, 2, 2, 3) + + table.attach(HIGEntryLabel(_('Services:')), 0, 1, 3, 4) + table.attach( + self.make_services_display(scaninfo['services']), 1, 2, 3, 4) + + hbox._pack_noexpand_nofill(hig_box_space_holder()) + hbox._pack_noexpand_nofill(table) + + return hbox + + def make_services_display(self, services): + """Return a widget displaying a list of services like + 1-1027,1029-1033,1040,1043,1050,1058-1059,1067-1068,1076,1080""" + combo = Gtk.ComboBoxText() + + for i in services.split(","): + combo.append_text(i) + + return combo + +if __name__ == "__main__": + import sys + from zenmapCore.NmapParser import NmapParser + + filename = sys.argv[1] + parsed = NmapParser() + parsed.parse_file(filename) + run_details = ScanRunDetailsPage(parsed) + window = Gtk.Window() + window.add(run_details) + window.connect("delete-event", lambda *args: Gtk.main_quit()) + window.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/ScanScanListPage.py b/zenmap/zenmapGUI/ScanScanListPage.py new file mode 100644 index 0000000..75d1670 --- /dev/null +++ b/zenmap/zenmapGUI/ScanScanListPage.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Pango + +from zenmapGUI.higwidgets.higboxes import HIGHBox, HIGVBox +from zenmapGUI.higwidgets.higbuttons import HIGButton +from zenmapGUI.higwidgets.higscrollers import HIGScrolledWindow +import zenmapCore.I18N # lgtm[py/unused-import] + + +def status_data_func(widget, cell_renderer, model, iter, data): + entry = model.get_value(iter, 0) + if entry.running: + status = _("Running") + elif entry.finished: + if entry.parsed is not None and entry.parsed.unsaved: + status = _("Unsaved") + else: + status = "" + elif entry.failed: + status = _("Failed") + elif entry.canceled: + status = _("Canceled") + cell_renderer.set_property("text", status) + + +def command_data_func(widget, cell_renderer, model, iter, data): + entry = model.get_value(iter, 0) + cell_renderer.set_property("ellipsize", Pango.EllipsizeMode.END) + cell_renderer.set_property("text", entry.get_command_string()) + + +class ScanScanListPage(HIGVBox): + """This is the "Scans" scan results tab. It the list of running and + finished scans contained in the ScansListStore passed to the + constructor.""" + def __init__(self, scans_store): + HIGVBox.__init__(self) + + self.set_spacing(4) + + scans_store.connect("row-changed", self._row_changed) + + self.scans_list = Gtk.TreeView.new_with_model(scans_store) + self.scans_list.get_selection().connect( + "changed", self._selection_changed) + + status_col = Gtk.TreeViewColumn(title=_("Status")) + cell = Gtk.CellRendererText() + status_col.pack_start(cell, True) + status_col.set_cell_data_func(cell, status_data_func) + self.scans_list.append_column(status_col) + + command_col = Gtk.TreeViewColumn(title=_("Command")) + cell = Gtk.CellRendererText() + command_col.pack_start(cell, True) + command_col.set_cell_data_func(cell, command_data_func) + self.scans_list.append_column(command_col) + + scrolled_window = HIGScrolledWindow() + scrolled_window.set_border_width(0) + scrolled_window.add(self.scans_list) + + self.pack_start(scrolled_window, True, True, 0) + + hbox = HIGHBox() + buttonbox = Gtk.ButtonBox.new(Gtk.Orientation.HORIZONTAL) + buttonbox.set_layout(Gtk.ButtonBoxStyle.START) + buttonbox.set_spacing(4) + + self.append_button = HIGButton(_("Append Scan"), Gtk.STOCK_ADD) + buttonbox.pack_start(self.append_button, False, True, 0) + + self.remove_button = HIGButton(_("Remove Scan"), Gtk.STOCK_REMOVE) + buttonbox.pack_start(self.remove_button, False, True, 0) + + self.cancel_button = HIGButton(_("Cancel Scan"), Gtk.STOCK_CANCEL) + buttonbox.pack_start(self.cancel_button, False, True, 0) + + hbox.pack_start(buttonbox, True, True, 4) + + self.pack_start(hbox, False, True, 4) + + self._update() + + def _row_changed(self, model, path, i): + self._update() + + def _selection_changed(self, selection): + self._update() + + def _update(self): + # Make the Cancel button sensitive or not depending on whether a + # running scan is selected. + tree_selection = self.scans_list.get_selection() + if tree_selection is None: + # I can't find anything in the PyGTK documentation that suggests + # this is possible, but we received many crash reports that + # indicate it is. + model, selection = None, [] + else: + model, selection = tree_selection.get_selected_rows() + + for path in selection: + entry = model.get_value(model.get_iter(path), 0) + if entry.running: + self.cancel_button.set_sensitive(True) + break + else: + self.cancel_button.set_sensitive(False) + + if len(selection) == 0: + self.remove_button.set_sensitive(False) + else: + self.remove_button.set_sensitive(True) diff --git a/zenmap/zenmapGUI/ScanToolbar.py b/zenmap/zenmapGUI/ScanToolbar.py new file mode 100644 index 0000000..e03459a --- /dev/null +++ b/zenmap/zenmapGUI/ScanToolbar.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.higwidgets.higboxes import HIGHBox +from zenmapGUI.higwidgets.higlabels import HIGEntryLabel + +import zenmapCore.I18N # lgtm[py/unused-import] + +from zenmapGUI.ProfileCombo import ProfileCombo +from zenmapGUI.TargetCombo import TargetCombo + + +class ScanCommandToolbar(HIGHBox): + """This class builds the toolbar devoted to Command entry. It allows you to + retrieve and edit the current command entered.""" + def __init__(self): + """Initialize command toolbar""" + HIGHBox.__init__(self) + + self.command_label = HIGEntryLabel(_("Command:")) + self.command_entry = Gtk.Entry() + + self._pack_noexpand_nofill(self.command_label) + self._pack_expand_fill(self.command_entry) + + def get_command(self): + """Retrieve command entry""" + return self.command_entry.get_text() + + def set_command(self, command): + """Set a command entry""" + self.command_entry.set_text(command) + + command = property(get_command, set_command) + + +class ScanToolbar(HIGHBox): + """ + This function regards the Scanning Toolbar, which includes + the Target and Profile editable fields/dropdown boxes, as well as + the Scan button and assigns events and and actions associated with + each. + """ + def __init__(self): + """Initialize Scan Toolbar, including Events, and packing all + of the GUI elements in layout""" + HIGHBox.__init__(self) + + self._create_target() + self._create_profile() + + self.scan_button = Gtk.Button.new_with_label(_("Scan")) + self.cancel_button = Gtk.Button.new_with_label(_("Cancel")) + + self._pack_noexpand_nofill(self.target_label) + self._pack_expand_fill(self.target_entry) + + self._pack_noexpand_nofill(self.profile_label) + self._pack_expand_fill(self.profile_entry) + + self._pack_noexpand_nofill(self.scan_button) + self._pack_noexpand_nofill(self.cancel_button) + + # Skip over the dropdown arrow so you can tab to the profile entry. + self.target_entry.set_focus_chain((self.target_entry.get_child(),)) + + self.target_entry.get_child().connect('activate', + lambda x: self.profile_entry.grab_focus()) + self.profile_entry.get_child().connect('activate', + lambda x: self.scan_button.clicked()) + + def _create_target(self): + """Create a target and update the list""" + self.target_label = HIGEntryLabel(_("Target:")) + self.target_entry = TargetCombo() + + self.update_target_list() + + def _create_profile(self): + """Create new profile and update list""" + self.profile_label = HIGEntryLabel(_('Profile:')) + self.profile_entry = ProfileCombo() + + self.update() + + def update_target_list(self): + self.target_entry.update() + + def add_new_target(self, target): + self.target_entry.add_new_target(target) + + def get_selected_target(self): + """Return currently selected target""" + return self.target_entry.selected_target + + def set_selected_target(self, target): + """Modify currently selected target""" + self.target_entry.selected_target = target + + def update(self): + self.profile_entry.update() + + def set_profiles(self, profiles): + """Modify profile""" + self.profile_entry.set_profiles(profiles) + + def get_selected_profile(self): + """Return currently selected profile""" + return self.profile_entry.selected_profile + + def set_selected_profile(self, profile): + """Modify currently selected profile""" + self.profile_entry.selected_profile = profile + + selected_profile = property(get_selected_profile, set_selected_profile) + selected_target = property(get_selected_target, set_selected_target) + +if __name__ == "__main__": + w = Gtk.Window() + box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + w.add(box) + + stool = ScanToolbar() + sctool = ScanCommandToolbar() + + box.pack_start(stool, True, True, 0) + box.pack_start(sctool, True, True, 0) + + w.connect("delete-event", lambda x, y: Gtk.main_quit()) + w.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/ScansListStore.py b/zenmap/zenmapGUI/ScansListStore.py new file mode 100644 index 0000000..6882fe0 --- /dev/null +++ b/zenmap/zenmapGUI/ScansListStore.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +class ScansListStoreEntry(object): + """This class is an abstraction for running and completed scans, which are + otherwise represented by very different classes.""" + + # Possible states for the scan to be in. + UNINITIALIZED, RUNNING, FINISHED, FAILED, CANCELED = list(range(5)) + + def __init__(self): + self.state = self.UNINITIALIZED + self.command = None + self.parsed = None + + def set_running(self, command=None): + self.state = self.RUNNING + self.command = command + + def set_finished(self, parsed=None): + self.state = self.FINISHED + self.parsed = parsed + + def set_failed(self): + self.state = self.FAILED + + def set_canceled(self): + self.state = self.CANCELED + + def get_command_string(self): + if self.parsed is not None: + return self.parsed.get_nmap_command() + elif self.command is not None: + return self.command.command + else: + return None + + running = property(lambda self: self.state == self.RUNNING) + finished = property(lambda self: self.state == self.FINISHED) + failed = property(lambda self: self.state == self.FAILED) + canceled = property(lambda self: self.state == self.CANCELED) + + +class ScansListStore(Gtk.ListStore): + """This is a specialization of a Gtk.ListStore that holds running, + completed, and failed scans.""" + def __init__(self): + Gtk.ListStore.__init__(self, object) + + def add_running_scan(self, command): + """Add a running NmapCommand object to the list of scans.""" + entry = ScansListStoreEntry() + entry.set_running(command) + return self.append([entry]) + + def finish_running_scan(self, command, parsed): + """Find an existing NmapCommand object and replace it with the given + parsed representation.""" + i = self._find_running_scan(command) + if i is not None: + entry = self.get_value(i, 0) + entry.set_finished(parsed) + path = self.get_path(i) + self.row_changed(path, i) + return i + + def fail_running_scan(self, command): + """Mark a running scan as failed.""" + i = self._find_running_scan(command) + if i is not None: + entry = self.get_value(i, 0) + entry.set_failed() + path = self.get_path(i) + self.row_changed(path, i) + return i + + def cancel_running_scan(self, command): + """Mark a running scan as canceled.""" + i = self._find_running_scan(command) + if i is not None: + entry = self.get_value(i, 0) + entry.set_canceled() + path = self.get_path(i) + self.row_changed(path, i) + return i + + def add_scan(self, parsed): + """Add a parsed NmapParser object to the list of scans.""" + entry = ScansListStoreEntry() + entry.set_finished(parsed) + return self.append([entry]) + + def _find_running_scan(self, command): + """Find the scan entry whose command is command.""" + i = self.get_iter_first() + while i is not None: + entry = self.get_value(i, 0) + if entry.command is command: + return i + i = self.iter_next(i) + return None diff --git a/zenmap/zenmapGUI/ScriptInterface.py b/zenmap/zenmapGUI/ScriptInterface.py new file mode 100644 index 0000000..b22651a --- /dev/null +++ b/zenmap/zenmapGUI/ScriptInterface.py @@ -0,0 +1,706 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +# This module is responsible for interface present under "Scripting" tab. + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib + +import os +import tempfile + +# Prevent loading PyXML +import xml +xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x] + +import xml.sax + +from zenmapGUI.higwidgets.higboxes import HIGVBox, HIGHBox +from zenmapGUI.higwidgets.higscrollers import HIGScrolledWindow +from zenmapGUI.higwidgets.higbuttons import HIGButton +from zenmapCore.ScriptMetadata import get_script_entries +from zenmapCore.ScriptArgsParser import parse_script_args_dict +from zenmapCore.NmapCommand import NmapCommand +from zenmapCore.NmapOptions import NmapOptions +import zenmapCore.NSEDocParser +import zenmapGUI.FileChoosers +from zenmapCore.UmitConf import PathsConfig +from zenmapCore.UmitLogging import log +from zenmapCore.Name import APP_NAME + +paths_config = PathsConfig() + + +def text_buffer_insert_nsedoc(buf, nsedoc): + """Inserts NSEDoc at the end of the buffer, with markup turned into proper + tags.""" + if not buf.get_tag_table().lookup("NSEDOC_CODE_TAG"): + buf.create_tag("NSEDOC_CODE_TAG", font="Monospace") + for event in zenmapCore.NSEDocParser.nsedoc_parse(nsedoc): + if event.type == "paragraph_start": + buf.insert(buf.get_end_iter(), "\n") + elif event.type == "paragraph_end": + buf.insert(buf.get_end_iter(), "\n") + elif event.type == "list_start": + buf.insert(buf.get_end_iter(), "\n") + elif event.type == "list_end": + pass + elif event.type == "list_item_start": + buf.insert(buf.get_end_iter(), "\u2022\u00a0") # bullet nbsp + elif event.type == "list_item_end": + buf.insert(buf.get_end_iter(), "\n") + elif event.type == "text": + buf.insert(buf.get_end_iter(), event.text) + elif event.type == "code": + buf.insert_with_tags_by_name( + buf.get_end_iter(), event.text, "NSEDOC_CODE_TAG") + + +class ScriptHelpXMLContentHandler (xml.sax.handler.ContentHandler): + """A very simple parser for --script-help XML output. This could extract + other information like categories and description, but all it gets is + filenames. (ScriptMetadata gets the other information.)""" + def __init__(self): + xml.sax.handler.ContentHandler.__init__(self) + self.script_filenames = [] + self.scripts_dir = None + self.nselib_dir = None + + def startElement(self, name, attrs): + if name == "directory": + if "name" not in attrs: + raise ValueError( + '"directory" element did not have "name" attribute') + dirname = attrs["name"] + if "path" not in attrs: + raise ValueError( + '"directory" element did not have "path" attribute') + path = attrs["path"] + if dirname == "scripts": + self.scripts_dir = path + elif dirname == "nselib": + self.nselib_dir = path + else: + # Ignore. + pass + elif name == "script": + if "filename" not in attrs: + raise ValueError( + '"script" element did not have "filename" attribute') + self.script_filenames.append(attrs["filename"]) + + @staticmethod + def parse_nmap_script_help(f): + parser = xml.sax.make_parser() + handler = ScriptHelpXMLContentHandler() + parser.setContentHandler(handler) + parser.parse(f) + return handler + + +class ScriptInterface: + # Timeout, in milliseconds, after the user stops typing and we update the + # interface from --script. + SCRIPT_LIST_DELAY = 500 + # Timeout, in milliseconds, between polls of the Nmap subprocess. + NMAP_DELAY = 200 + + def __init__(self, root_tabs, ops, update_command, help_buf): + self.hmainbox = HIGHBox(False, 0) + self.notscripttab = False # show profile editor it is a script tab + self.nmap_process = None + self.script_list_timeout_id = None + self.nmap_timeout_id = None + self.chk_nmap_timeout_id = None + self.script_file_chooser = None + self.ops = ops + self.update_command = update_command + self.help_buf = help_buf + self.arg_values = {} + self.current_arguments = [] + self.set_help_texts() + self.prev_script_spec = None + self.focusedentry = None + + self.liststore = Gtk.ListStore.new([str, bool, object]) + + self.file_liststore = Gtk.ListStore.new([str, bool]) + + # Arg name, arg value, (name, desc) tuple. + self.arg_liststore = Gtk.ListStore.new([str, str, object]) + + # This is what is shown initially. After the initial Nmap run to get + # the list of script is finished, this will be replaced with a TreeView + # showing the scripts or an error message. + self.script_list_container = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + self.script_list_container.pack_start(self.make_please_wait_widget(), True, True, 0) + self.hmainbox.pack_start(self.script_list_container, False, False, 0) + + self.nmap_error_widget = Gtk.Label.new(_( + "There was an error getting the list of scripts from Nmap. " + "Try upgrading Nmap.")) + self.nmap_error_widget.set_line_wrap(True) + self.nmap_error_widget.show_all() + + self.script_list_widget = self.make_script_list_widget() + self.script_list_widget.show_all() + + vbox = HIGVBox(False, 5) + vbox.pack_start(self.make_description_widget(), True, True, 0) + vbox.pack_start(self.make_arguments_widget(), True, True, 0) + self.hmainbox.pack_end(vbox, True, True, 0) + + self.update_argument_values(self.ops["--script-args"]) + + # Start the initial backgrounded Nmap run to get the list of all + # available scripts. + self.get_script_list("all", self.initial_script_list_cb) + + def get_script_list(self, rules, callback): + """Start an Nmap subprocess in the background with + "--script-help=<rules> -oX -", and set it up to call the given callback + when finished.""" + + ops = NmapOptions() + ops.executable = paths_config.nmap_command_path + ops["--script-help"] = rules + ops["-oX"] = "-" + command_string = ops.render_string() + # Separate stderr to avoid breaking XML parsing with "Warning: File + # ./nse_main.lua exists, but Nmap is using...". + stderr = tempfile.TemporaryFile( + mode="r", prefix=APP_NAME + "-script-help-stderr-") + log.debug("Script interface: running %s" % repr(command_string)) + nmap_process = NmapCommand(command_string) + try: + nmap_process.run_scan(stderr=stderr) + except Exception as e: + callback(False, None) + stderr.close() + return + stderr.close() + + self.script_list_widget.set_sensitive(False) + + GLib.timeout_add( + self.NMAP_DELAY, self.script_list_timer_callback, + nmap_process, callback) + + def script_list_timer_callback(self, process, callback): + try: + status = process.scan_state() + except Exception: + status = None + log.debug("Script interface: script_list_timer_callback %s" % + repr(status)) + + if status is True: + # Still running, schedule this timer to check again. + return True + + self.script_list_widget.set_sensitive(True) + + if status is False: + # Finished with success. + callback(True, process) + else: + # Finished with error. + callback(False, process) + + def initial_script_list_cb(self, status, process): + log.debug("Script interface: initial_script_list_cb %s" % repr(status)) + for child in self.script_list_container.get_children(): + self.script_list_container.remove(child) + if status and self.handle_initial_script_list_output(process): + self.script_list_container.pack_start(self.script_list_widget, True, True, 0) + else: + self.script_list_container.pack_start(self.nmap_error_widget, True, True, 0) + + def handle_initial_script_list_output(self, process): + process.stdout_file.seek(0) + try: + handler = ScriptHelpXMLContentHandler.parse_nmap_script_help( + process.stdout_file) + except (ValueError, xml.sax.SAXParseException) as e: + log.debug("--script-help parse exception: %s" % str(e)) + return False + + # Check if any scripts were output; if not, Nmap is probably too old. + if len(handler.script_filenames) == 0: + return False + + if not handler.scripts_dir: + log.debug("--script-help error: no scripts directory") + return False + if not handler.nselib_dir: + log.debug("--script-help error: no nselib directory") + return False + + log.debug("Script interface: scripts dir %s" % repr( + handler.scripts_dir)) + log.debug("Script interface: nselib dir %s" % repr(handler.nselib_dir)) + + # Make a dict of script metadata entries. + entries = {} + for entry in get_script_entries( + handler.scripts_dir, handler.nselib_dir): + entries[entry.filename] = entry + + self.liststore.clear() + for filename in handler.script_filenames: + basename = os.path.basename(filename) + entry = entries.get(basename) + if entry: + script_id = self.strip_file_name(basename) + self.liststore.append([script_id, False, entry]) + else: + # ScriptMetadata found nothing for this script? + self.file_liststore.append([filename, False]) + + # Now figure out which scripts are selected. + self.update_script_list_from_spec(self.ops["--script"]) + return True + + def update_script_list_from_spec(self, spec): + """Callback method for user edit delay.""" + log.debug("Script interface: update_script_list_from_spec %s" % repr( + spec)) + if spec: + self.get_script_list(spec, self.update_script_list_cb) + else: + self.refresh_list_scripts([]) + + def update_script_list_cb(self, status, process): + log.debug("Script interface: update_script_list_cb %s" % repr(status)) + if status: + self.handle_update_script_list_output(process) + else: + self.refresh_list_scripts([]) + + def handle_update_script_list_output(self, process): + process.stdout_file.seek(0) + try: + handler = ScriptHelpXMLContentHandler.parse_nmap_script_help( + process.stdout_file) + except (ValueError, xml.sax.SAXParseException) as e: + log.debug("--script-help parse exception: %s" % str(e)) + return False + + self.refresh_list_scripts(handler.script_filenames) + + def get_hmain_box(self): + """Returns main Hbox to ProfileEditor.""" + return self.hmainbox + + def update(self): + """Updates the interface when the command entry is changed.""" + # updates list of scripts + rules = self.ops["--script"] + if (self.prev_script_spec != rules): + self.renew_script_list_timer(rules) + self.prev_script_spec = rules + # updates arguments.. + raw_argument = self.ops["--script-args"] + if raw_argument is not None: + self.parse_script_args(raw_argument) + self.arg_liststore.clear() + for arg in self.current_arguments: + arg_name, arg_desc = arg + value = self.arg_values.get(arg_name) + if not value: + self.arg_liststore.append([arg_name, None, arg]) + else: + self.arg_liststore.append([arg_name, value, arg]) + + def renew_script_list_timer(self, spec): + """Restart the timer to update the script list when the user edits the + command. Because updating the script list is an expensive operation + involving the creation of a subprocess, we don't do it for every typed + character.""" + if self.script_list_timeout_id: + GLib.source_remove(self.script_list_timeout_id) + self.script_list_timeout_id = GLib.timeout_add( + self.SCRIPT_LIST_DELAY, + self.update_script_list_from_spec, spec) + + def parse_script_args(self, raw_argument): + """When the command line is edited, this function is called to update + the script arguments display according to the value of + --script-args.""" + arg_dict = parse_script_args_dict(raw_argument) + if arg_dict is None: # if there is parsing error args_dict holds none + self.arg_values.clear() + else: + for key in arg_dict.keys(): + self.arg_values[key] = arg_dict[key] + + def update_argument_values(self, raw_argument): + """When scripting tab starts up, argument values are updated.""" + if raw_argument is not None: + self.parse_script_args(raw_argument) + + def set_help_texts(self): + """Sets the help texts to be displayed.""" + self.list_scripts_help = _("""List of scripts + +A list of all installed scripts. Activate or deactivate a script \ +by clicking the box next to the script name.""") + self.description_help = _("""Description + +This box shows the categories a script belongs to. In addition, it gives a \ +detailed description of the script which is present in script. A URL points \ +to online NSEDoc documentation.""") + self.argument_help = _("""Arguments + +A list of arguments that affect the selected script. Enter a value by \ +clicking in the value field beside the argument name.""") + + def make_please_wait_widget(self): + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + label = Gtk.Label.new(_("Please wait.")) + label.set_line_wrap(True) + vbox.pack_start(label, True, True, 0) + return vbox + + def make_script_list_widget(self): + """Creates and packs widgets associated with left hand side of + Interface.""" + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + + scrolled_window = HIGScrolledWindow() + scrolled_window.set_policy(Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS) + # Expand only vertically. + scrolled_window.set_size_request(175, -1) + listview = Gtk.TreeView.new_with_model(self.liststore) + listview.set_headers_visible(False) + listview.connect("enter-notify-event", self.update_help_ls_cb) + selection = listview.get_selection() + selection.connect("changed", self.selection_changed_cb) + cell = Gtk.CellRendererText() + togglecell = Gtk.CellRendererToggle() + togglecell.set_property("activatable", True) + togglecell.connect("toggled", self.toggled_cb, self.liststore) + col = Gtk.TreeViewColumn(title=_('Names')) + col.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY) + col.set_resizable(True) + togglecol = Gtk.TreeViewColumn(title=None, cell_renderer=togglecell) + togglecol.add_attribute(togglecell, "active", 1) + listview.append_column(togglecol) + listview.append_column(col) + col.pack_start(cell, True) + col.add_attribute(cell, "text", 0) + scrolled_window.add(listview) + scrolled_window.show() + vbox.pack_start(scrolled_window, True, True, 0) + + self.file_scrolled_window = HIGScrolledWindow() + self.file_scrolled_window.set_policy( + Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS) + self.file_scrolled_window.set_size_request(175, -1) + self.file_scrolled_window.hide() + self.file_scrolled_window.set_no_show_all(True) + + self.file_listview = Gtk.TreeView.new_with_model(self.file_liststore) + self.file_listview.set_headers_visible(False) + col = Gtk.TreeViewColumn(title=None) + self.file_listview.append_column(col) + cell = Gtk.CellRendererToggle() + col.pack_start(cell, True) + cell.set_property("activatable", True) + col.add_attribute(cell, "active", 1) + cell.connect("toggled", self.toggled_cb, self.file_liststore) + + col = Gtk.TreeViewColumn(title=None) + self.file_listview.append_column(col) + cell = Gtk.CellRendererText() + col.pack_start(cell, True) + col.add_attribute(cell, "text", 0) + + self.file_listview.show_all() + self.file_scrolled_window.add(self.file_listview) + vbox.pack_start(self.file_scrolled_window, False, True, 0) + + hbox = HIGHBox(False, 2) + self.remove_file_button = HIGButton(stock=Gtk.STOCK_REMOVE) + self.remove_file_button.connect( + "clicked", self.remove_file_button_clicked_cb) + self.remove_file_button.set_sensitive(False) + hbox.pack_end(self.remove_file_button, True, True, 0) + add_file_button = HIGButton(stock=Gtk.STOCK_ADD) + add_file_button.connect("clicked", self.add_file_button_clicked_cb) + hbox.pack_end(add_file_button, True, True, 0) + + vbox.pack_start(hbox, False, False, 0) + + return vbox + + def refresh_list_scripts(self, selected_scripts): + """The list of selected scripts is refreshed in the list store.""" + for row in self.liststore: + row[1] = False + for row in self.file_liststore: + row[1] = False + for filename in selected_scripts: + for row in self.liststore: + if row[0] == self.strip_file_name(os.path.basename(filename)): + row[1] = True + break + else: + for row in self.file_liststore: + if row[0] == filename: + row[1] = True + break + else: + self.file_liststore.append([filename, True]) + + def strip_file_name(self, filename): + """Removes a ".nse" extension from filename if present.""" + if(filename.endswith(".nse")): + return filename[:-4] + else: + return filename + + def set_script_from_selection(self): + scriptsname = [] + for entry in self.liststore: + if entry[1]: + scriptsname.append(self.strip_file_name(entry[0])) + for entry in self.file_liststore: + if entry[1]: + scriptsname.append(entry[0]) + if len(scriptsname) == 0: + self.ops["--script"] = None + else: + self.ops["--script"] = ",".join(scriptsname) + self.update_command() + + def toggled_cb(self, cell, path, model): + """Callback method, called when the check box in list of scripts is + toggled.""" + model[path][1] = not model[path][1] + self.set_script_from_selection() + + def make_description_widget(self): + """Creates and packs widgets related to displaying the description + box.""" + sw = HIGScrolledWindow() + sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) + sw.set_shadow_type(Gtk.ShadowType.OUT) + sw.set_border_width(5) + text_view = Gtk.TextView() + text_view.connect("enter-notify-event", self.update_help_desc_cb) + self.text_buffer = text_view.get_buffer() + self.text_buffer.create_tag("Usage", font="Monospace") + self.text_buffer.create_tag("Output", font="Monospace") + text_view.set_wrap_mode(Gtk.WrapMode.WORD) + text_view.set_editable(False) + text_view.set_justification(Gtk.Justification.LEFT) + sw.add(text_view) + return sw + + def make_arguments_widget(self): + """Creates and packs widgets related to arguments box.""" + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + vbox.pack_start(Gtk.Label.new(_("Arguments")), False, False, 0) + arg_window = HIGScrolledWindow() + arg_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) + arg_window.set_shadow_type(Gtk.ShadowType.OUT) + + arg_listview = Gtk.TreeView.new_with_model(self.arg_liststore) + arg_listview.connect("motion-notify-event", self.update_help_arg_cb) + argument = Gtk.CellRendererText() + self.value = Gtk.CellRendererText() + self.value.connect("edited", self.value_edited_cb, self.arg_liststore) + arg_col = Gtk.TreeViewColumn(title="Arguments\t") + val_col = Gtk.TreeViewColumn(title="values") + arg_listview.append_column(arg_col) + arg_listview.append_column(val_col) + arg_col.pack_start(argument, True) + arg_col.add_attribute(argument, "text", 0) + val_col.pack_start(self.value, True) + val_col.add_attribute(self.value, "text", 1) + + arg_window.add(arg_listview) + vbox.pack_start(arg_window, True, True, 0) + + return vbox + + def value_edited_cb(self, cell, path, new_text, model): + """Called when the argument cell is edited.""" + self.arg_list = [] + model[path][1] = new_text + argument_name = model[path][0] + self.arg_values[argument_name] = new_text + self.update_arg_values() + + def update_arg_values(self): + """When the widget is updated with argument value, correspondingly + update the command line.""" + for key in self.arg_values.keys(): + if len(self.arg_values[key]) == 0: + del self.arg_values[key] + else: + self.arg_list.append(key + "=" + self.arg_values[key]) + if len(self.arg_list) == 0: + self.ops["--script-args"] = None + self.arg_values.clear() + else: + self.ops["--script-args"] = ",".join(self.arg_list) + self.update_command() + + def selection_changed_cb(self, selection): + """Called back when the list of scripts is selected.""" + model, selection = selection.get_selected_rows() + for path in selection: + entry = model.get_value(model.get_iter(path), 2) + self.set_description(entry) + self.populate_arg_list(entry) + # Remember the currently pointing script entry + self.focusedentry = entry + + def update_help_ls_cb(self, widget, extra): # list of scripts + """Callback method to display the help for the list of scripts.""" + self.help_buf.set_text(self.list_scripts_help) + + def update_help_desc_cb(self, widget, extra): + """Callback method for displaying description.""" + self.help_buf.set_text(self.description_help) + + def update_help_arg_cb(self, treeview, event): + """Callback method for displaying argument help.""" + wx, wy = treeview.get_pointer() + x, y = treeview.convert_widget_to_bin_window_coords(wx, wy) + path = treeview.get_path_at_pos(x, y) + if not path or not self.focusedentry: + self.help_buf.set_text("") + return + path = path[0] + model, selected = treeview.get_selection().get_selected() + arg_name, arg_desc = model.get_value(model.get_iter(path), 2) + if arg_desc is not None: + self.help_buf.set_text("") + self.help_buf.insert( + self.help_buf.get_end_iter(), text="%s\n" % arg_name) + text_buffer_insert_nsedoc(self.help_buf, arg_desc) + else: + self.help_buf.set_text("") + + def add_file_button_clicked_cb(self, button): + if self.script_file_chooser is None: + self.script_file_chooser = \ + zenmapGUI.FileChoosers.ScriptFileChooserDialog( + title=_("Select script files")) + response = self.script_file_chooser.run() + filenames = self.script_file_chooser.get_filenames() + self.script_file_chooser.hide() + if response != Gtk.ResponseType.OK: + return + for filename in filenames: + self.file_liststore.append([filename, True]) + if len(self.file_liststore) > 0: + self.file_scrolled_window.show() + self.remove_file_button.set_sensitive(True) + self.set_script_from_selection() + + def remove_file_button_clicked_cb(self, button): + selection = self.file_listview.get_selection() + model, selection = selection.get_selected_rows() + for path in selection: + self.file_liststore.remove(model.get_iter(path)) + if len(self.file_liststore) == 0: + self.file_scrolled_window.hide() + self.remove_file_button.set_sensitive(False) + self.set_script_from_selection() + + def set_description(self, entry): + """Sets the content that is to be displayed in the description box.""" + self.text_buffer.set_text("") + + self.text_buffer.insert(self.text_buffer.get_end_iter(), """\ +Categories: %(cats)s +""" % {"cats": ", ".join(entry.categories)}) + text_buffer_insert_nsedoc(self.text_buffer, entry.description) + if entry.usage: + self.text_buffer.insert( + self.text_buffer.get_end_iter(), "\nUsage\n") + self.text_buffer.insert_with_tags_by_name( + self.text_buffer.get_end_iter(), entry.usage, "Usage") + if entry.output: + self.text_buffer.insert( + self.text_buffer.get_end_iter(), "\nOutput\n") + self.text_buffer.insert_with_tags_by_name( + self.text_buffer.get_end_iter(), entry.output, "Output") + if entry.url: + self.text_buffer.insert( + self.text_buffer.get_end_iter(), "\n" + entry.url) + + def populate_arg_list(self, entry): + """Called when a particular script is hovered over to display its + arguments and values (if any).""" + self.arg_liststore.clear() + self.current_arguments = [] + self.value.set_property('editable', True) + for arg in entry.arguments: + arg_name, arg_desc = arg + self.current_arguments.append(arg) + value = self.arg_values.get(arg_name) + if not value: + self.arg_liststore.append([arg_name, None, arg]) + else: + self.arg_liststore.append([arg_name, value, arg]) diff --git a/zenmap/zenmapGUI/SearchGUI.py b/zenmap/zenmapGUI/SearchGUI.py new file mode 100644 index 0000000..f313073 --- /dev/null +++ b/zenmap/zenmapGUI/SearchGUI.py @@ -0,0 +1,915 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import re +import copy + +from zenmapGUI.higwidgets.higbuttons import HIGButton, HIGToggleButton +from zenmapGUI.higwidgets.higboxes import HIGHBox +from zenmapGUI.higwidgets.higlabels import HIGSectionLabel, HintWindow +from zenmapGUI.higwidgets.higdialogs import HIGAlertDialog + +import datetime + +from zenmapCore.Name import APP_DISPLAY_NAME +import zenmapCore.I18N # lgtm[py/unused-import] +from zenmapCore.NmapOptions import split_quoted +from zenmapCore.SearchResult import SearchDir, SearchDB, SearchDummy +from zenmapCore.UmitConf import SearchConfig + +from zenmapGUI.FileChoosers import DirectoryChooserDialog + +search_config = SearchConfig() + + +class SearchParser(object): + """This class is responsible for parsing the search string, and updating + the search dictionary (which is, in turn, passed to classes that perform + the actual search). It holds a reference to the SearchGUI object, which is + used to access its search_dict dictionary, so that all dictionary handling + is performed here. It is also responsible for adding additional directories + to the SearchGUI object via the 'dir:' operator.""" + + def __init__(self, search_gui, search_keywords): + self.search_gui = search_gui + self.search_dict = search_gui.search_dict + + # We need to make an operator->searchkey mapping, since the search + # entry field and the search classes have different syntax. + # + # NOTE: if you want to add a new search key not handled by the + # SearchResult class, you should add a new method match_CRITERIANAME to + # the SearchResult class. For example, if you'd like a "noodles" + # criteria, you need to create the method + # SearchResult.match_noodles(self, noodles_string). To see how searches + # are actually performed, start reading from the SearchResult.search() + # method. + self.ops2keys = copy.deepcopy(search_keywords) + + # This is not really an operator (see below) + self.ops2keys["dir"] = "dir" + + def update(self, search): + """Updates the search dictionary by parsing the input string.""" + + # Kill leftover keys and parse again. SLOW? Not really. + self.search_dict.clear() + + for word in split_quoted(search): + if word.find(":") != -1: + # We have an operator in our word, so we make the part left of + # the semicolon a key, and the part on the right a value + op, arg = word.split(":", 1) + if op in self.ops2keys: + key = self.ops2keys[op] + if key in self.search_dict: + self.search_dict[key].append(arg) + else: + self.search_dict[key] = [arg] + else: + # Just a simple keyword + if "keyword" in self.search_dict: + self.search_dict["keyword"].append(word) + else: + self.search_dict["keyword"] = [word] + + # Check if we have any dir: operators in our map, and if so, add them + # to the search_gui object and remove them from the map. The dir: + # operator isn't a real operator, in a sense that it doesn't need to be + # processed by the SearchResult.search() function. It is needed only to + # create a new SearchDir object, which is then used to perform the + # actual search(). + self.search_gui.init_search_dirs(self.search_dict.pop("dir", [])) + + +class SearchGUI(Gtk.Box, object): + """This class is a VBox that holds the search entry field and buttons on + top, and the results list on the bottom. The "Cancel" and "Open" buttons + are a part of the SearchWindow class, not SearchGUI.""" + def __init__(self, search_window): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + + self._create_widgets() + self._pack_widgets() + self._connect_events() + + # Search options + self.options = {} + self.options["file_extension"] = search_config.file_extension + self.options["directory"] = search_config.directory + self.options["search_db"] = search_config.search_db + + self.parsed_results = {} + self._set_result_view() + self.id = 0 + self.search_window = search_window + + # The Search* objects are created once per Search Window invocation, so + # that they get a list of scans only once, not whenever the search + # conditions change + if self.options["search_db"]: + try: + self.search_db = SearchDB() + except ImportError as e: + self.search_db = SearchDummy() + self.no_db_warning.show() + self.no_db_warning.set_text( + 'Warning: The database of saved scans is not ' + 'available. (%s.) Use "Include Directory" under ' + '"Expressions" to search a directory.' % str(e)) + + # Search directories can be added via the "dir:" operator, so it needs + # to be a map + self.search_dirs = {} + self.init_search_dirs() + + # We create an empty search dictionary, since SearchParser will fill it + # with keywords as it encounters different operators in the search + # string. + self.search_dict = dict() + # We need to define our own keyword search dictionary + search_keywords = dict() + search_keywords["keyword"] = "keyword" + search_keywords["profile"] = "profile" + search_keywords["pr"] = "profile" + search_keywords["target"] = "target" + search_keywords["t"] = "target" + search_keywords["option"] = "option" + search_keywords["o"] = "option" + search_keywords["date"] = "date" + search_keywords["d"] = "date" + search_keywords["after"] = "after" + search_keywords["a"] = "after" + search_keywords["before"] = "before" + search_keywords["b"] = "before" + search_keywords["os"] = "os" + search_keywords["scanned"] = "scanned" + search_keywords["sp"] = "scanned" + search_keywords["open"] = "open" + search_keywords["op"] = "open" + search_keywords["closed"] = "closed" + search_keywords["cp"] = "closed" + search_keywords["filtered"] = "filtered" + search_keywords["fp"] = "filtered" + search_keywords["unfiltered"] = "unfiltered" + search_keywords["ufp"] = "unfiltered" + search_keywords["open|filtered"] = "open_filtered" + search_keywords["ofp"] = "open_filtered" + search_keywords["closed|filtered"] = "closed_filtered" + search_keywords["cfp"] = "closed_filtered" + search_keywords["service"] = "service" + search_keywords["s"] = "service" + search_keywords["inroute"] = "in_route" + search_keywords["ir"] = "in_route" + self.search_parser = SearchParser(self, search_keywords) + + # This list holds the (operator, argument) tuples, parsed from the GUI + # criteria rows + self.gui_criteria_list = [] + + # Do an initial "empty" search, so that the results window initially + # holds all scans in the database + self.search_parser.update("") + self.start_search() + + def init_search_dirs(self, dirs=[]): + # Start fresh + self.search_dirs.clear() + + # If specified, add the search directory from the Zenmap config file to + # the map + conf_dir = self.options["directory"] + if conf_dir: + self.search_dirs[conf_dir] = SearchDir( + conf_dir, self.options["file_extension"]) + + # Process any other dirs (as added by the dir: operator) + for dir in dirs: + self.search_dirs[dir] = SearchDir( + dir, self.options["file_extension"]) + + def _create_widgets(self): + # Search box and buttons + self.search_top_hbox = HIGHBox() + self.search_label = HIGSectionLabel(_("Search:")) + self.search_entry = Gtk.Entry() + self.expressions_btn = HIGToggleButton( + _("Expressions "), Gtk.STOCK_EDIT) + + # The quick reference tooltip button + self.search_tooltip_btn = HIGButton(" ", Gtk.STOCK_INFO) + + # The expression VBox. This is only visible once the user clicks on + # "Expressions". The expressions (if any) should be tightly packed so + # that they don't take too much screen real-estate + self.expr_vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + + # Results section + self.result_list = Gtk.ListStore.new([str, str, int]) # title, date, id + self.result_view = Gtk.TreeView.new_with_model(self.result_list) + self.result_scrolled = Gtk.ScrolledWindow() + self.result_title_column = Gtk.TreeViewColumn(title=_("Scan")) + self.result_date_column = Gtk.TreeViewColumn(title=_("Date")) + + self.no_db_warning = Gtk.Label() + self.no_db_warning.set_line_wrap(True) + self.no_db_warning.set_no_show_all(True) + + self.expr_window = None + + def _pack_widgets(self): + # Packing label, search box and buttons + self.search_top_hbox.set_spacing(4) + self.search_top_hbox.pack_start(self.search_label, False, True, 0) + self.search_top_hbox.pack_start(self.search_entry, True, True, 0) + self.search_top_hbox.pack_start(self.expressions_btn, False, True, 0) + self.search_top_hbox.pack_start(self.search_tooltip_btn, False, True, 0) + + # Packing the result section + self.result_scrolled.add(self.result_view) + self.result_scrolled.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + + # Packing it all together + self.set_spacing(4) + self.pack_start(self.search_top_hbox, False, True, 0) + self.pack_start(self.expr_vbox, False, True, 0) + self.pack_start(self.result_scrolled, True, True, 0) + self.pack_start(self.no_db_warning, False, True, 0) + + def _connect_events(self): + self.search_entry.connect("changed", self.update_search_entry) + self.search_tooltip_btn.connect("clicked", self.show_quick_help) + self.expressions_btn.connect("toggled", self.expressions_clicked) + + def show_quick_help(self, widget=None, extra=None): + hint_window = HintWindow(QUICK_HELP_TEXT) + hint_window.show_all() + + def expressions_clicked(self, widget=None, extra=None): + if (len(self.expr_vbox.get_children()) == 0 and + self.search_entry.get_text() == ""): + # This is the first time the user has clicked on "Show Expressions" + # and the search entry box is empty, so we add a single Criterion + # row + self.expr_vbox.pack_start(Criterion(self), True, True, 0) + + if self.expressions_btn.get_active(): + # The Expressions GUI is about to be displayed. It needs to reflect + # all the conditions in the search entry field, so a comparison + # between the entry field and the GUI needs to be performed. + + # Make the search entry field insensitive while expressions are + # visible + self.search_entry.set_sensitive(False) + + # Get a map of operator => argument from the Expressions GUI so + # that we can compare them with the ones in the search entry field + gui_ops = {} + for criterion in self.expr_vbox.get_children(): + if criterion.operator in gui_ops: + gui_ops[criterion.operator].append(criterion.argument) + else: + gui_ops[criterion.operator] = [criterion.argument] + + # We compare the search entry field to the Expressions GUI. Every + # (operator, value) pair must be present in the GUI after this loop + # is done. + for op, args in self.search_dict.items(): + for arg in args: + if (op not in gui_ops) or (arg not in gui_ops[op]): + # We need to add this pair to the GUI + self.expr_vbox.pack_start( + Criterion(self, op, arg), False, True, 0) + + # Now we check if there are any leftover criterion rows that aren't + # present in the search_dict (for example, if a user has deleted + # something from the search entry field) + for criterion in self.expr_vbox.get_children(): + if (criterion.operator not in self.search_dict or + criterion.argument not in self.search_dict[ + criterion.operator]): + criterion.destroy() + # If we have deleted all rows, add an empty one + if len(self.expr_vbox.get_children()) == 0: + self.expr_vbox.pack_start(Criterion(self), True, True, 0) + + # Display all elements + self.expr_vbox.show_all() + else: + # The Expressions GUI is about to be hidden. No updates to the + # search entry field are necessary, since it gets updated on every + # change in one of the criterion rows. + self.expr_vbox.hide() + self.search_entry.set_sensitive(True) + + def close(self): + if self.expr_window is not None: + self.expr_window.close() + + def add_criterion(self, caller): + # We need to find where the caller (Criteria object) is located among + # all the rows, so that we can insert the new row after it + caller_index = self.expr_vbox.get_children().index(caller) + + # Make a new Criteria row and insert it after the calling row + criteria = Criterion(self, "keyword") + self.expr_vbox.pack_start(criteria, False, True, 0) + self.expr_vbox.reorder_child(criteria, caller_index + 1) + criteria.show_all() + + def remove_criterion(self, c): + if len(self.expr_vbox.get_children()) > 1: + c.destroy() + self.criterion_changed() + + def criterion_changed(self): + # We go through all criteria rows and make a new search string + search_string = "" + for criterion in self.expr_vbox.get_children(): + if criterion.operator != "keyword": + search_string += criterion.operator + ":" + search_string += criterion.argument.replace(" ", "") + " " + + self.search_entry.set_text(search_string.strip()) + + self.search_parser.update(self.search_entry.get_text()) + self.start_search() + + def add_search_dir(self, dir): + if dir not in self.search_dirs: + self.search_dirs[dir] = SearchDir( + dir, self.options["file_extension"]) + + def update_search_entry(self, widget, extra=None): + """Called when the search entry field is modified.""" + self.search_parser.update(widget.get_text()) + self.start_search() + + def start_search(self): + if not self.options["search_db"] and not self.options["directory"]: + d = HIGAlertDialog( + message_format=_("No search method selected!"), + secondary_text=_( + "%s can search results on directories or inside its " + "own database. Please select a method by choosing a " + "directory or by checking the search data base option " + "in the 'Search options' tab before starting a search" + ) % APP_DISPLAY_NAME) + d.run() + d.destroy() + return + + self.clear_result_list() + + matched = 0 + total = 0 + if self.options["search_db"]: + total += len(self.search_db.get_scan_results()) + for result in self.search_db.search(**self.search_dict): + self.append_result(result) + matched += 1 + + for search_dir in self.search_dirs.values(): + total += len(search_dir.get_scan_results()) + for result in search_dir.search(**self.search_dict): + self.append_result(result) + matched += 1 + + #total += len(self.search_tabs.get_scan_results()) + #for result in self.search_tabs.search(**self.search_dict): + # self.append_result(result) + # matched += 1 + + self.search_window.set_label_text( + "Matched <b>%s</b> out of <b>%s</b> scans." % ( + str(matched), str(total))) + + def clear_result_list(self): + for i in range(len(self.result_list)): + iter = self.result_list.get_iter_first() + del(self.result_list[iter]) + + def append_result(self, parsed_result): + title = parsed_result.scan_name + + try: + date = datetime.datetime.fromtimestamp(float(parsed_result.start)) + date_field = date.strftime("%Y-%m-%d %H:%M") + except ValueError: + date_field = _("Unknown") + + self.parsed_results[self.id] = [title, parsed_result] + self.result_list.append([title, date_field, self.id]) + self.id += 1 + + def get_selected_results(self): + selection = self.result_view.get_selection() + rows = selection.get_selected_rows() + list_store = rows[0] + + results = {} + for row in rows[1]: + r = row[0] + results[list_store[r][2]] = self.parsed_results[list_store[r][2]] + + return results + + def _set_result_view(self): + self.result_view.set_enable_search(True) + self.result_view.set_search_column(0) + + selection = self.result_view.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + + self.result_view.append_column(self.result_title_column) + self.result_view.append_column(self.result_date_column) + + self.result_title_column.set_resizable(True) + self.result_title_column.set_min_width(200) + self.result_date_column.set_resizable(True) + + self.result_title_column.set_sort_column_id(0) + self.result_date_column.set_sort_column_id(1) + + self.result_title_column.set_reorderable(True) + self.result_date_column.set_reorderable(True) + + cell = Gtk.CellRendererText() + + self.result_title_column.pack_start(cell, True) + self.result_date_column.pack_start(cell, True) + + self.result_title_column.set_attributes(cell, text=0) + self.result_date_column.set_attributes(cell, text=1) + + selected_results = property(get_selected_results) + + +class Criterion(Gtk.Box): + """This class holds one criterion row, represented as an HBox. It holds a + ComboBox and a Subcriterion's subclass instance, depending on the selected + entry in the ComboBox. For example, when the 'Target' option is selected, a + SimpleSubcriterion widget is displayed, but when the 'Date' operator is + selected, a DateSubcriterion widget is displayed.""" + + def __init__(self, search_window, operator="keyword", argument=""): + """A reference to the search window is passed so that we can call + add_criterion and remove_criterion.""" + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + + self.search_window = search_window + self.default_operator = operator + self.default_argument = argument + + # We need this as a map, so that we can pass the operator into + # the SimpleSubcriterion instance + self.combo_entries = {"Keyword": ["keyword"], + "Profile Name": ["profile"], + "Target": ["target"], + "Options": ["option"], + "Date": ["date", "after", "before"], + "Operating System": ["os"], + "Port": ["open", "scanned", "closed", "filtered", + "unfiltered", "open_filtered", + "closed_filtered"], + "Service": ["service"], + "Host In Route": ["inroute"], + "Include Directory": ["dir"]} + + self._create_widgets() + self._pack_widgets() + self._connect_events() + + def _create_widgets(self): + # A ComboBox containing the list of operators + self.operator_combo = Gtk.ComboBoxText() + + # Sort all the keys from combo_entries and make an entry for each of + # them + sorted_entries = list(self.combo_entries.keys()) + sorted_entries.sort() + for name in sorted_entries: + self.operator_combo.append_text(name) + + # Select the default operator + for entry, operators in self.combo_entries.items(): + for operator in operators: + if operator == self.default_operator: + self.operator_combo.set_active(sorted_entries.index(entry)) + break + + # Create a subcriterion + self.subcriterion = self.new_subcriterion( + self.default_operator, self.default_argument) + + # The "add" and "remove" buttons + self.add_btn = HIGButton(" ", Gtk.STOCK_ADD) + self.remove_btn = HIGButton(" ", Gtk.STOCK_REMOVE) + + def _pack_widgets(self): + self.pack_start(self.operator_combo, False, True, 0) + self.pack_start(self.subcriterion, True, True, 0) + self.pack_start(self.add_btn, False, True, 0) + self.pack_start(self.remove_btn, False, True, 0) + + def _connect_events(self): + self.operator_combo.connect("changed", self.operator_changed) + self.add_btn.connect("clicked", self.add_clicked) + self.remove_btn.connect("clicked", self.remove_clicked) + + def get_operator(self): + return self.subcriterion.operator + + def get_argument(self): + return self.subcriterion.argument + + def add_clicked(self, widget=None, extra=None): + self.search_window.add_criterion(self) + + def remove_clicked(self, widget=None, extra=None): + self.search_window.remove_criterion(self) + + def value_changed(self, op, arg): + """Subcriterion instances call this method when something changes + inside of them.""" + # We let the search window know about the change + self.search_window.criterion_changed() + + def new_subcriterion(self, operator="keyword", argument=""): + if operator in self.combo_entries["Date"]: + return DateSubcriterion(operator, argument) + elif operator in self.combo_entries["Port"]: + return PortSubcriterion(operator, argument) + elif operator == "dir": + return DirSubcriterion(operator, argument) + else: + return SimpleSubcriterion(operator, argument) + + def operator_changed(self, widget=None, extra=None): + """This function is called when the user selects a different entry in + the Criterion's ComboBox.""" + # Destroy the previous subcriterion + self.subcriterion.destroy() + + # Create a new subcriterion depending on the selected operator + selected = self.operator_combo.get_active_text() + operator = self.combo_entries[selected][0] + self.subcriterion = self.new_subcriterion(operator) + + # Pack it, and place it on the right side of the ComboBox + self.pack_start(self.subcriterion, True, True, 0) + self.reorder_child(self.subcriterion, 1) + + # Notify the search window about the change + self.search_window.criterion_changed() + + # Good to go + self.subcriterion.show_all() + + operator = property(get_operator) + argument = property(get_argument) + + +class Subcriterion(Gtk.Box): + """This class is a base class for all subcriterion types. Depending on the + criterion selected in the Criterion's ComboBox, a subclass of Subcriterion + is created to display the appropriate GUI.""" + def __init__(self): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + + self.operator = "" + self.argument = "" + + def value_changed(self): + """Propagates the operator and the argument up to the Criterion + parent.""" + self.get_parent().value_changed(self.operator, self.argument) + + +class SimpleSubcriterion(Subcriterion): + """This class represents all 'simple' criterion types that need only an + entry box in order to define the criterion.""" + def __init__(self, operator="keyword", argument=""): + Subcriterion.__init__(self) + + self.operator = operator + self.argument = argument + + self._create_widgets() + self._pack_widgets() + self._connect_widgets() + + def _create_widgets(self): + self.entry = Gtk.Entry() + if self.argument: + self.entry.set_text(self.argument) + + def _pack_widgets(self): + self.pack_start(self.entry, True, True, 0) + + def _connect_widgets(self): + self.entry.connect("changed", self.entry_changed) + + def entry_changed(self, widget=None, extra=None): + self.argument = widget.get_text() + self.value_changed() + + +class PortSubcriterion(Subcriterion): + """This class shows the port criterion GUI.""" + def __init__(self, operator="open", argument=""): + Subcriterion.__init__(self) + + self.operator = operator + self.argument = argument + + self._create_widgets() + self._pack_widgets() + self._connect_widgets() + + def _create_widgets(self): + self.entry = Gtk.Entry() + if self.argument: + self.entry.set_text(self.argument) + + self.label = Gtk.Label.new(" is ") + + self.port_state_combo = Gtk.ComboBoxText() + states = ["open", "scanned", "closed", "filtered", "unfiltered", + "open|filtered", "closed|filtered"] + for state in states: + self.port_state_combo.append_text(state) + self.port_state_combo.set_active( + states.index(self.operator.replace("_", "|"))) + + def _pack_widgets(self): + self.pack_start(self.entry, True, True, 0) + self.pack_start(self.label, False, True, 0) + self.pack_start(self.port_state_combo, False, True, 0) + + def _connect_widgets(self): + self.entry.connect("changed", self.entry_changed) + self.port_state_combo.connect("changed", self.port_criterion_changed) + + def entry_changed(self, widget=None, extra=None): + self.argument = widget.get_text() + self.value_changed() + + def port_criterion_changed(self, widget=None, extra=None): + self.operator = widget.get_active_text() + self.value_changed() + + +class DirSubcriterion(Subcriterion): + def __init__(self, operator="dir", argument=""): + Subcriterion.__init__(self) + + self.operator = operator + self.argument = argument + + self._create_widgets() + self._pack_widgets() + self._connect_widgets() + + def _create_widgets(self): + self.dir_entry = Gtk.Entry() + if self.argument: + self.dir_entry.set_text(self.argument) + self.chooser_btn = HIGButton("Choose...", Gtk.STOCK_OPEN) + + def _pack_widgets(self): + self.pack_start(self.dir_entry, True, True, 0) + self.pack_start(self.chooser_btn, False, True, 0) + + def _connect_widgets(self): + self.chooser_btn.connect("clicked", self.choose_clicked) + self.dir_entry.connect("changed", self.dir_entry_changed) + + def choose_clicked(self, widget=None, extra=None): + # Display a directory chooser dialog + chooser_dlg = DirectoryChooserDialog("Include folder in search") + + if chooser_dlg.run() == Gtk.ResponseType.OK: + self.dir_entry.set_text(chooser_dlg.get_filename()) + + chooser_dlg.destroy() + + def dir_entry_changed(self, widget=None, extra=None): + self.argument = widget.get_text() + self.value_changed() + + +class DateSubcriterion(Subcriterion): + def __init__(self, operator="date", argument=""): + Subcriterion.__init__(self) + + self.text2op = {"is": "date", + "after": "after", + "before": "before"} + + self.operator = operator + + self._create_widgets() + self._pack_widgets() + self._connect_widgets() + + # Count the fuzzy operators, so that we can append them to the argument + # later + self.fuzzies = argument.count("~") + argument = argument.replace("~", "") + self.minus_notation = False + if re.match(r"\d\d\d\d-\d\d-\d\d$", argument) is not None: + year, month, day = argument.split("-") + self.date = datetime.date(int(year), int(month), int(day)) + self.argument = argument + elif re.match(r"[-|\+]\d+$", argument) is not None: + # Convert the date from the "-n" notation into YYYY-MM-DD + parsed_date = datetime.date.fromordinal( + datetime.date.today().toordinal() + int(argument)) + self.argument = argument + self.date = datetime.date( + parsed_date.year, parsed_date.month, parsed_date.day) + + self.minus_notation = True + else: + self.date = datetime.date.today() + self.argument = self.date.isoformat() + + # Append fuzzy operators, if any + self.argument += "~" * self.fuzzies + + def _create_widgets(self): + self.date_criterion_combo = Gtk.ComboBoxText() + self.date_criterion_combo.append_text("is") + self.date_criterion_combo.append_text("after") + self.date_criterion_combo.append_text("before") + if self.operator == "date": + self.date_criterion_combo.set_active(0) + elif self.operator == "after": + self.date_criterion_combo.set_active(1) + else: + self.date_criterion_combo.set_active(2) + self.date_button = HIGButton() + + def _pack_widgets(self): + self.pack_start(self.date_criterion_combo, False, True, 0) + self.pack_start(self.date_button, True, True, 0) + + def _connect_widgets(self): + self.date_criterion_combo.connect( + "changed", self.date_criterion_changed) + self.date_button.connect("clicked", self.show_calendar) + + def date_criterion_changed(self, widget=None, extra=None): + self.operator = self.text2op[widget.get_active_text()] + + # Let the parent know that the operator has changed + self.value_changed() + + def show_calendar(self, widget): + calendar = DateCalendar() + calendar.connect_calendar(self.update_button) + calendar.show_all() + + def update_button(self, widget): + cal_date = widget.get_date() + # Add 1 to month because Gtk.Calendar date is zero-based. + self.date = datetime.date(cal_date[0], cal_date[1] + 1, cal_date[2]) + + # Set the argument, using the search format + if self.minus_notation: + # We need to calculate the date's offset from today, so that we can + # represent the date in the "-n" notation + today = datetime.date.today() + offset = self.date.toordinal() - today.toordinal() + if offset > 0: + self.argument = "+" + str(offset) + else: + self.argument = str(offset) + else: + self.argument = self.date.isoformat() + self.argument += "~" * self.fuzzies + + # Let the parent know about the change + self.value_changed() + + def set_date(self, date): + self.date_button.set_label(date.strftime("%d %b %Y")) + self._date = date + + def get_date(self): + return self._date + + date = property(get_date, set_date) + _date = datetime.date.today() + + +class DateCalendar(Gtk.Window, object): + def __init__(self): + Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) + self.set_position(Gtk.WindowPosition.MOUSE) + + self.calendar = Gtk.Calendar() + self.add(self.calendar) + + def connect_calendar(self, update_button_cb): + self.calendar.connect("day-selected-double-click", + self.kill_calendar, update_button_cb) + + def kill_calendar(self, widget, method): + method(widget) + self.destroy() + +QUICK_HELP_TEXT = _("""\ +Entering the text into the search performs a <b>keyword search</b> - the \ +search string is matched against the entire output of each scan. + +To refine the search, you can use <b>operators</b> to search only within \ +a specific part of a scan. Operators can be added to the search \ +interactively if you click on the <b>Expressions</b> button, or you can \ +enter them manually into the search field. Most operators have a short \ +form, listed. + +<b>profile: (pr:)</b> - Profile used. +<b>target: (t:)</b> - User-supplied target, or a rDNS result. +<b>option: (o:)</b> - Scan options. +<b>date: (d:)</b> - The date when scan was performed. Fuzzy matching is \ +possible using the "~" suffix. Each "~" broadens the search by one day \ +on "each side" of the date. In addition, it is possible to use the \ +\"date:-n\" notation which means "n days ago". +<b>after: (a:)</b> - Matches scans made after the supplied date \ +(<i>YYYY-MM-DD</i> or <i>-n</i>). +<b>before (b:)</b> - Matches scans made before the supplied \ +date(<i>YYYY-MM-DD</i> or <i>-n</i>). +<b>os:</b> - All OS-related fields. +<b>scanned: (sp:)</b> - Matches a port if it was among those scanned. +<b>open: (op:)</b> - Open ports discovered in a scan. +<b>closed: (cp:)</b> - Closed ports discovered in a scan. +<b>filtered: (fp:)</b> - Filtered ports discovered in scan. +<b>unfiltered: (ufp:)</b> - Unfiltered ports found in a scan (using, for \ +example, an ACK scan). +<b>open|filtered: (ofp:)</b> - Ports in the \"open|filtered\" state. +<b>closed|filtered: (cfp:)</b> - Ports in the \"closed|filtered\" state. +<b>service: (s:)</b> - All service-related fields. +<b>inroute: (ir:)</b> - Matches a router in the scan's traceroute output. +""") diff --git a/zenmap/zenmapGUI/SearchWindow.py b/zenmap/zenmapGUI/SearchWindow.py new file mode 100644 index 0000000..2d8791b --- /dev/null +++ b/zenmap/zenmapGUI/SearchWindow.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.SearchGUI import SearchGUI + +import zenmapCore.I18N # lgtm[py/unused-import] +from zenmapCore.UmitConf import is_maemo + +from zenmapGUI.higwidgets.higboxes import HIGVBox +from zenmapGUI.higwidgets.higbuttons import HIGButton + +BaseSearchWindow = None +hildon = None + +if is_maemo(): + import hildon + + class BaseSearchWindow(hildon.Window): + def __init__(self): + hildon.Window.__init__(self) + + def _pack_widgets(self): + pass +else: + class BaseSearchWindow(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self) + self.set_title(_("Search Scans")) + self.set_position(Gtk.WindowPosition.CENTER) + + def _pack_widgets(self): + self.vbox.set_border_width(4) + + +class SearchWindow(BaseSearchWindow): + def __init__(self, load_method, append_method): + BaseSearchWindow.__init__(self) + + self.set_default_size(600, 400) + + self.load_method = load_method + self.append_method = append_method + + self._create_widgets() + self._pack_widgets() + self._connect_widgets() + + def _create_widgets(self): + self.vbox = HIGVBox() + + self.bottom_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4) + self.bottom_label = Gtk.Label() + self.btn_box = Gtk.ButtonBox.new(Gtk.Orientation.HORIZONTAL) + self.btn_open = HIGButton(stock=Gtk.STOCK_OPEN) + self.btn_append = HIGButton(_("Append"), Gtk.STOCK_ADD) + self.btn_close = HIGButton(stock=Gtk.STOCK_CLOSE) + + self.search_gui = SearchGUI(self) + + def _pack_widgets(self): + BaseSearchWindow._pack_widgets(self) + + self.btn_box.set_layout(Gtk.ButtonBoxStyle.END) + self.btn_box.set_spacing(4) + self.btn_box.pack_start(self.btn_close, True, True, 0) + self.btn_box.pack_start(self.btn_append, True, True, 0) + self.btn_box.pack_start(self.btn_open, True, True, 0) + + self.bottom_label.set_alignment(0.0, 0.5) + self.bottom_label.set_use_markup(True) + + self.bottom_hbox.pack_start(self.bottom_label, True, True, 0) + self.bottom_hbox.pack_start(self.btn_box, False, True, 0) + + self.vbox.set_spacing(4) + self.vbox.pack_start(self.search_gui, True, True, 0) + self.vbox.pack_start(self.bottom_hbox, False, True, 0) + + self.add(self.vbox) + + def _connect_widgets(self): + # Double click on result, opens it + self.search_gui.result_view.connect( + "row-activated", self.open_selected) + + self.btn_open.connect("clicked", self.open_selected) + self.btn_append.connect("clicked", self.append_selected) + self.btn_close.connect("clicked", self.close) + self.connect("delete-event", self.close) + + def close(self, widget=None, event=None): + self.search_gui.close() + self.destroy() + + def set_label_text(self, text): + self.bottom_label.set_label(text) + + def open_selected(self, widget=None, path=None, view_column=None, + extra=None): + # Open selected results + self.load_method(self.results) + + # Close Search Window + self.close() + + def append_selected(self, widget=None, path=None, view_column=None, + extra=None): + # Append selected results + self.append_method(self.results) + + # Close Search Window + self.close() + + def get_results(self): + # Return list with parsed objects from result list store + return self.search_gui.selected_results + + results = property(get_results) + + +if __name__ == "__main__": + search = SearchWindow(lambda x: Gtk.main_quit(), lambda x: Gtk.main_quit()) + search.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/TargetCombo.py b/zenmap/zenmapGUI/TargetCombo.py new file mode 100644 index 0000000..3e55ec1 --- /dev/null +++ b/zenmap/zenmapGUI/TargetCombo.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapCore.TargetList import target_list + + +class TargetCombo(Gtk.ComboBoxText): + def __init__(self): + Gtk.ComboBoxText.__init__(self, has_entry=True) + + self.completion = Gtk.EntryCompletion() + self.get_child().set_completion(self.completion) + self.completion.set_model(self.get_model()) + self.completion.set_text_column(0) + + self.update() + + def update(self): + self.remove_all() + + t_list = target_list.get_target_list() + for target in t_list[:15]: + self.append_text(target.replace('\n', '')) + + def add_new_target(self, target): + target_list.add_target(target) + self.update() + + def get_selected_target(self): + return self.get_child().get_text() + + def set_selected_target(self, target): + self.get_child().set_text(target) + + selected_target = property(get_selected_target, set_selected_target) + +if __name__ == "__main__": + w = Gtk.Window() + t = TargetCombo() + w.add(t) + + w.connect("delete-event", lambda x, y: Gtk.main_quit()) + w.show_all() + Gtk.main() diff --git a/zenmap/zenmapGUI/TopologyPage.py b/zenmap/zenmapGUI/TopologyPage.py new file mode 100644 index 0000000..c4f82bc --- /dev/null +++ b/zenmap/zenmapGUI/TopologyPage.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from zenmapGUI.higwidgets.higboxes import HIGVBox + +import radialnet.gui.RadialNet as RadialNet +from radialnet.gui.ControlWidget import ControlWidget, ControlFisheye +from radialnet.gui.Toolbar import Toolbar +from radialnet.util.integration import make_graph_from_hosts + + +SLOW_LIMIT = 1000 + + +class TopologyPage(HIGVBox): + def __init__(self, inventory): + HIGVBox.__init__(self) + + self.set_border_width(6) + self.set_spacing(4) + + self.network_inventory = inventory + + self._create_widgets() + self._pack_widgets() + + def _create_widgets(self): + self.rn_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4) + self.rn_vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + + # RadialNet's widgets + self.radialnet = RadialNet.RadialNet(RadialNet.LAYOUT_WEIGHTED) + self.control = ControlWidget(self.radialnet) + self.fisheye = ControlFisheye(self.radialnet) + self.rn_toolbar = Toolbar(self.radialnet, + self, + self.control, + self.fisheye) + + self.display_panel = HIGVBox() + + self.radialnet.set_no_show_all(True) + + self.slow_vbox = HIGVBox() + self.slow_label = Gtk.Label() + self.slow_vbox.pack_start(self.slow_label, False, False, 0) + show_button = Gtk.Button.new_with_label(_("Show the topology anyway")) + show_button.connect("clicked", self.show_anyway) + self.slow_vbox.pack_start(show_button, False, False, 0) + self.slow_vbox.show_all() + self.slow_vbox.set_no_show_all(True) + self.slow_vbox.hide() + + self.radialnet.show() + + def _pack_widgets(self): + self.rn_hbox.pack_start(self.display_panel, True, True, 0) + self.rn_hbox.pack_start(self.control, False, True, 0) + + self.rn_vbox.pack_start(self.rn_hbox, True, True, 0) + self.rn_vbox.pack_start(self.fisheye, False, True, 0) + + self.pack_start(self.rn_toolbar, False, False, 0) + self.pack_start(self.rn_vbox, True, True, 0) + + self.display_panel.pack_start(self.slow_vbox, True, False, 0) + self.display_panel.pack_start(self.radialnet, True, True, 0) + + def add_scan(self, scan): + """Parses a given XML file and adds the parsed result to the network + inventory.""" + self.network_inventory.add_scan(scan) + self.update_radialnet() + + def update_radialnet(self): + """Creates a graph from network inventory's host list and displays + it.""" + hosts_up = self.network_inventory.get_hosts_up() + + self.slow_label.set_text(_("""\ +Topology is disabled because too many hosts can cause it +to run slowly. The limit is %d hosts and there are %d.\ +""" % (SLOW_LIMIT, len(hosts_up)))) + + if len(hosts_up) <= SLOW_LIMIT: + self.radialnet.show() + self.slow_vbox.hide() + self.update_radialnet_unchecked() + else: + self.radialnet.hide() + self.slow_vbox.show() + + def update_radialnet_unchecked(self): + hosts_up = self.network_inventory.get_hosts_up() + graph = make_graph_from_hosts(hosts_up) + self.radialnet.set_empty() + self.radialnet.set_graph(graph) + self.radialnet.show() + + def show_anyway(self, widget): + self.radialnet.show() + self.slow_vbox.hide() + self.update_radialnet_unchecked() diff --git a/zenmap/zenmapGUI/__init__.py b/zenmap/zenmapGUI/__init__.py new file mode 100644 index 0000000..cf74df4 --- /dev/null +++ b/zenmap/zenmapGUI/__init__.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ diff --git a/zenmap/zenmapGUI/higwidgets/__init__.py b/zenmap/zenmapGUI/higwidgets/__init__.py new file mode 100644 index 0000000..bfb7866 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/__init__.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/__init__.py + +This module implements GTK Widgets that try their best to adhere to the +GNOME Human Interface Guidelines (aka HIG). + +This is mostly implemented by subclassing from the GTK classes, and +providing defaults that better match the HIG specifications/recommendations. +""" + +from .gtkutils import * +from .higboxes import * +from .higbuttons import * +from .higdialogs import * +from .higentries import * +from .higexpanders import * +from .higlabels import * +from .higlogindialogs import * +from .higprogressbars import * +from .higscrollers import * +from .higspinner import * +from .higtables import * +from .higtextviewers import * +from .higwindows import * diff --git a/zenmap/zenmapGUI/higwidgets/gtkutils.py b/zenmap/zenmapGUI/higwidgets/gtkutils.py new file mode 100644 index 0000000..1a89acb --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/gtkutils.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/gtkutils.py + + gtk related functions +""" + +__all__ = ['gtk_version_major', 'gtk_version_minor', 'gtk_version_release', + 'gtk_constant_name'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +# version information +gtk_version_major, gtk_version_minor, gtk_version_release = gi.version_info +assert gtk_version_major == 3 + + +def gtk_constant_name(group, value): + """ + Returns the (py)GTK+ name of a constant, given its group name + """ + group_response = {-1: 'Gtk.ResponseType.NONE', + -2: 'Gtk.ResponseType.REJECT', + -3: 'Gtk.ResponseType.ACCEPT', + -4: 'Gtk.ResponseType.DELETE_EVENT', + -5: 'Gtk.ResponseType.OK', + -6: 'Gtk.ResponseType.CANCEL', + -7: 'Gtk.ResponseType.CLOSE', + -8: 'Gtk.ResponseType.YES', + -9: 'Gtk.ResponseType.NO', + -10: 'Gtk.ResponseType.APPLY', + -11: 'Gtk.ResponseType.HELP'} + + groups = {'response': group_response} + + return groups.get(group, {}).get(value, 'Error: constant value not found') diff --git a/zenmap/zenmapGUI/higwidgets/higboxes.py b/zenmap/zenmapGUI/higwidgets/higboxes.py new file mode 100644 index 0000000..bb76472 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higboxes.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higboxes.py + + box related classes +""" + +__all__ = ['HIGHBox', 'HIGVBox'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +class HIGBox(Gtk.Box): + def _pack_noexpand_nofill(self, widget): + self.pack_start(widget, False, False, 0) + + def _pack_expand_fill(self, widget): + self.pack_start(widget, True, True, 0) + + def add(self, widget): + # Make default packing arguments same as before (Gtk.Box has expand + # set to False by default). + self.pack_start(widget, True, True, 0) + + +class HIGHBox(HIGBox): + def __init__(self, homogeneous=False, spacing=12): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, + homogeneous=homogeneous, spacing=spacing) + + pack_section_label = HIGBox._pack_noexpand_nofill + pack_label = HIGBox._pack_noexpand_nofill + pack_entry = HIGBox._pack_expand_fill + + +class HIGVBox(HIGBox): + def __init__(self, homogeneous=False, spacing=12): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, + homogeneous=homogeneous, spacing=spacing) + + # Packs a widget as a line, so it doesn't expand vertically + pack_line = HIGBox._pack_noexpand_nofill + + +class HIGSpacer(HIGHBox): + def __init__(self, widget=None): + HIGHBox.__init__(self) + self.set_spacing(6) + + self._pack_noexpand_nofill(hig_box_space_holder()) + + if widget: + self._pack_expand_fill(widget) + self.child = widget + + def get_child(self): + return self.child + + +def hig_box_space_holder(): + return Gtk.Label.new(" ") diff --git a/zenmap/zenmapGUI/higwidgets/higbuttons.py b/zenmap/zenmapGUI/higwidgets/higbuttons.py new file mode 100644 index 0000000..1f47c6d --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higbuttons.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higbuttons.py + + button related classes +""" + +__all__ = ['HIGButton', 'HIGToggleButton'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +class HIGMixButton(Gtk.Box): + def __init__(self, title, stock): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, + homogeneous=False, spacing=4) + self.img = Gtk.Image() + self.img.set_from_stock(stock, Gtk.IconSize.BUTTON) + + self.lbl = Gtk.Label.new(title) + + self.hbox1 = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2) + self.hbox1.set_homogeneous(False) + self.hbox1.pack_start(self.img, False, False, 0) + self.hbox1.pack_start(self.lbl, False, False, 0) + + self.align = Gtk.Alignment.new(0.5, 0.5, 0, 0) + self.pack_start(self.align, True, True, 0) + self.pack_start(self.hbox1, True, True, 0) + + +class HIGButton(Gtk.Button): + def __init__(self, title="", stock=None): + if title and stock: + Gtk.Button.__init__(self) + content = HIGMixButton(title, stock) + self.add(content) + elif title and not stock: + Gtk.Button.__init__(self, label=title) + elif stock: + Gtk.Button.__init__(self, stock=stock) + else: + Gtk.Button.__init__(self) + + +class HIGToggleButton(Gtk.ToggleButton): + def __init__(self, title="", stock=None): + if title and stock: + Gtk.ToggleButton.__init__(self) + content = HIGMixButton(title, stock) + self.add(content) + elif title and not stock: + Gtk.ToggleButton.__init__(self, label=title) + elif stock: + Gtk.ToggleButton.__init__(self, stock=stock) + self.set_use_stock(True) + else: + Gtk.ToggleButton.__init__(self) diff --git a/zenmap/zenmapGUI/higwidgets/higdialogs.py b/zenmap/zenmapGUI/higwidgets/higdialogs.py new file mode 100644 index 0000000..bb64614 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higdialogs.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higdialogs.py + + dialog related classes +""" + +__all__ = ['HIGDialog', 'HIGAlertDialog'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +class HIGDialog(Gtk.Dialog): + """ + HIGFied Dialog + """ + def __init__(self, title='', parent=None, flags=0, buttons=()): + Gtk.Dialog.__init__(self, title=title, parent=parent, flags=flags) + self.set_border_width(5) + self.vbox.set_border_width(2) + self.vbox.set_spacing(6) + + if buttons: + self.add_buttons(*buttons) + + +class HIGAlertDialog(Gtk.MessageDialog): + """ + HIGfied Alert Dialog. + + Implements the suggestions documented in: + http://developer.gnome.org/projects/gup/hig/2.0/windows-alert.html + """ + + def __init__(self, parent=None, flags=0, type=Gtk.MessageType.INFO, + # HIG mandates that every Alert should have an "affirmative + # button that dismisses the alert and performs the action + # suggested" + buttons=Gtk.ButtonsType.OK, + message_format=None, + secondary_text=None): + + Gtk.MessageDialog.__init__(self, parent=parent, flags=flags, + message_type=type, buttons=buttons) + + self.set_resizable(False) + + # HIG mandates that Message Dialogs should have no title: + # "Alert windows have no titles, as the title would usually + # unnecessarily duplicate the alert's primary text" + self.set_title("") + self.set_markup( + "<span weight='bold'size='larger'>%s</span>" % message_format) + if secondary_text: + self.format_secondary_text(secondary_text) + + +if __name__ == '__main__': + + from higlabels import HIGEntryLabel, HIGDialogLabel + + # HIGDialog + d = HIGDialog(title='HIGDialog', + buttons=(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)) + dialog_label = HIGDialogLabel('A HIGDialogLabel on a HIGDialog') + dialog_label.show() + d.vbox.pack_start(dialog_label, True, True, 0) + + entry_label = HIGEntryLabel('A HIGEntryLabel on a HIGDialog') + entry_label.show() + d.vbox.pack_start(entry_label, True, True, 0) + + d.run() + d.destroy() + + # HIGAlertDialog + d = HIGAlertDialog(message_format="You Have and Appointment in 15 minutes", + secondary_text="You shouldn't be late this time. " + "Oh, and there's a huge traffic jam on your way!") + d.run() + d.destroy() diff --git a/zenmap/zenmapGUI/higwidgets/higentries.py b/zenmap/zenmapGUI/higwidgets/higentries.py new file mode 100644 index 0000000..54fe75f --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higentries.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higentries.py + + entries related classes +""" + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +__all__ = ('HIGTextEntry', 'HIGPasswordEntry') + +HIGTextEntry = Gtk.Entry + + +class HIGPasswordEntry(HIGTextEntry): + """ + An entry that masks its text + """ + def __init__(self): + HIGTextEntry.__init__(self) + self.set_visibility(False) diff --git a/zenmap/zenmapGUI/higwidgets/higexpanders.py b/zenmap/zenmapGUI/higwidgets/higexpanders.py new file mode 100644 index 0000000..9c27cdc --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higexpanders.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higexpanders.py + + expanders related classes +""" + +__all__ = ['HIGExpander'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from .higboxes import HIGHBox, hig_box_space_holder + + +class HIGExpander(Gtk.Expander): + def __init__(self, label): + Gtk.Expander.__init__(self) + + self.set_use_markup(True) + self.set_label(label) + + self.hbox = HIGHBox() + self.hbox.set_border_width(5) + self.hbox._pack_noexpand_nofill(hig_box_space_holder()) + + self.add(self.hbox) + + def get_container(self): + return self.hbox diff --git a/zenmap/zenmapGUI/higwidgets/higframe.py b/zenmap/zenmapGUI/higwidgets/higframe.py new file mode 100644 index 0000000..293f819 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higframe.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higframe.py + + hig frame +""" + +__all__ = ['HIGFrame'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +class HIGFrame(Gtk.Frame): + """ + Frame without border with bold label. + """ + def __init__(self, label=None): + Gtk.Frame.__init__(self) + + self.set_shadow_type(Gtk.ShadowType.NONE) + self._flabel = Gtk.Label() + self._set_label(label) + self.set_label_widget(self._flabel) + + def _set_label(self, label): + self._flabel.set_markup("<b>%s</b>" % label) + +# Demo +if __name__ == "__main__": + w = Gtk.Window() + + hframe = HIGFrame("Sample HIGFrame") + aalign = Gtk.Alignment.new(0, 0, 0, 0) + aalign.set_padding(12, 0, 24, 0) + abox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + aalign.add(abox) + hframe.add(aalign) + w.add(hframe) + + for i in range(5): + abox.pack_start(Gtk.Label.new("Sample %d" % i), False, False, 3) + + w.connect('destroy', lambda d: Gtk.main_quit()) + w.show_all() + + Gtk.main() diff --git a/zenmap/zenmapGUI/higwidgets/higlabels.py b/zenmap/zenmapGUI/higwidgets/higlabels.py new file mode 100644 index 0000000..ac2dcfc --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higlabels.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higlabels.py + + labels related classes +""" + +__all__ = [ + 'HIGSectionLabel', 'HIGHintSectionLabel', 'HIGEntryLabel', 'HIGDialogLabel' + ] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk + + +class HIGSectionLabel(Gtk.Label): + """ + Bold label, used to define sections + """ + def __init__(self, text=None): + Gtk.Label.__init__(self) + if text: + self.set_markup("<b>%s</b>" % (text)) + self.set_justify(Gtk.Justification.LEFT) + self.props.xalign = 0 + self.props.yalign = 0.5 + self.set_line_wrap(True) + + +class HIGHintSectionLabel(Gtk.Box, object): + """ + Bold label used to define sections, with a little icon that shows up a hint + when mouse is over it. + """ + def __init__(self, text=None, hint=None): + Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL) + + self.label = HIGSectionLabel(text) + self.hint = Hint(hint) + + self.pack_start(self.label, False, False, 0) + self.pack_start(self.hint, False, False, 5) + + +class Hint(Gtk.EventBox, object): + def __init__(self, hint): + Gtk.EventBox.__init__(self) + self.hint = hint + + self.hint_image = Gtk.Image() + self.hint_image.set_from_icon_name( + "dialog-information", Gtk.IconSize.SMALL_TOOLBAR) + + self.add(self.hint_image) + + self.connect("button-press-event", self.show_hint) + + def show_hint(self, widget, event=None): + hint_window = HintWindow(self.hint) + hint_window.show_all() + + +class HintWindow(Gtk.Window): + def __init__(self, hint): + Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) + self.set_position(Gtk.WindowPosition.MOUSE) + self.set_resizable(False) + + bg_color = Gdk.RGBA() + bg_color.parse("#fbff99") + self.override_background_color(Gtk.StateFlags.NORMAL, bg_color) + + self.event = Gtk.EventBox() + self.event.override_background_color(Gtk.StateFlags.NORMAL, bg_color) + self.event.set_border_width(10) + self.event.connect("button-press-event", self.close) + + self.hint_label = Gtk.Label.new(hint) + self.hint_label.set_use_markup(True) + self.hint_label.set_line_wrap(True) + self.hint_label.set_max_width_chars(52) + self.hint_label.props.xalign = 0 + self.hint_label.props.yalign = 0.5 + + self.event.add(self.hint_label) + self.add(self.event) + + def close(self, widget, event=None): + self.destroy() + + +class HIGEntryLabel(Gtk.Label): + """ + Simple label, like the ones used to label entries + """ + def __init__(self, text=None): + Gtk.Label.__init__(self, label=text) + self.set_justify(Gtk.Justification.LEFT) + self.props.xalign = 0 + self.props.yalign = 0.5 + self.set_use_markup(True) + self.set_line_wrap(True) + + +class HIGDialogLabel(Gtk.Label): + """ + Centered, line-wrappable label, usually used on dialogs. + """ + def __init__(self, text=None): + Gtk.Label.__init__(self, label=text) + self.set_justify(Gtk.Justification.CENTER) + self.set_use_markup(True) + self.set_line_wrap(True) + +if __name__ == "__main__": + w = Gtk.Window() + h = HIGHintSectionLabel("Label", "Hint") + w.add(h) + w.connect("delete-event", lambda x, y: Gtk.main_quit()) + w.show_all() + + Gtk.main() diff --git a/zenmap/zenmapGUI/higwidgets/higlogindialogs.py b/zenmap/zenmapGUI/higwidgets/higlogindialogs.py new file mode 100644 index 0000000..a940c60 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higlogindialogs.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higlogindialog.py + + a basic login/authentication dialog +""" + +__all__ = ['HIGLoginDialog'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from .higdialogs import HIGDialog +from .higlabels import HIGEntryLabel +from .higtables import HIGTable +from .higentries import HIGTextEntry, HIGPasswordEntry + + +class HIGLoginDialog(HIGDialog): + """ + A dialog that asks for basic login information (username / password) + """ + def __init__(self, title='Login', + buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)): + HIGDialog.__init__(self, title, buttons=buttons) + + self.username_label = HIGEntryLabel("Username:") + self.username_entry = HIGTextEntry() + self.password_label = HIGEntryLabel("Password:") + self.password_entry = HIGPasswordEntry() + + self.username_password_table = HIGTable(2, 2) + self.username_password_table.attach_label(self.username_label, + 0, 1, 0, 1) + self.username_password_table.attach_entry(self.username_entry, + 1, 2, 0, 1) + self.username_password_table.attach_label(self.password_label, + 0, 1, 1, 2) + self.username_password_table.attach_entry(self.password_entry, + 1, 2, 1, 2) + + self.vbox.pack_start(self.username_password_table, False, False, 0) + self.set_default_response(Gtk.ResponseType.ACCEPT) + + def run(self): + self.show_all() + return HIGDialog.run(self) + +if __name__ == '__main__': + + from gtkutils import gtk_constant_name + + # HIGLoginDialog + d = HIGLoginDialog() + response_value = d.run() + print(gtk_constant_name('response', response_value)) + d.destroy() diff --git a/zenmap/zenmapGUI/higwidgets/hignotebooks.py b/zenmap/zenmapGUI/higwidgets/hignotebooks.py new file mode 100644 index 0000000..0f4b262 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/hignotebooks.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GObject + +from .higboxes import HIGHBox +from .higbuttons import HIGButton + + +class HIGNotebook(Gtk.Notebook): + def __init__(self): + Gtk.Notebook.__init__(self) + self.popup_enable() + + +class HIGClosableTabLabel(HIGHBox): + __gsignals__ = { + 'close-clicked': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()) + } + + def __init__(self, label_text=""): + GObject.GObject.__init__(self) + #HIGHBox.__init__(self, spacing=4) + + self.label_text = label_text + self.__create_widgets() + + #self.property_map = {"label_text" : self.label.get_label} + + def __create_widgets(self): + self.label = Gtk.Label.new(self.label_text) + self.close_image = Gtk.Image() + self.close_image.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.BUTTON) + self.close_button = HIGButton() + self.close_button.set_size_request(20, 20) + self.close_button.set_relief(Gtk.ReliefStyle.NONE) + self.close_button.set_focus_on_click(False) + self.close_button.add(self.close_image) + + self.close_button.connect('clicked', self.__close_button_clicked) + + for w in (self.label, self.close_button): + self.pack_start(w, False, False, 0) + + self.show_all() + + # def do_get_property(self, property): + # func = self.property_map.get(property, None) + # if func: + # return func() + # else: + # raise + + def __close_button_clicked(self, data): + self.emit('close-clicked') + + def get_text(self): + return self.label.get_text() + + def set_text(self, text): + self.label.set_text(text) + + def get_label(self): + return self.label.get_label() + + def set_label(self, label): + self.label.set_text(label) + +GObject.type_register(HIGClosableTabLabel) + +HIGAnimatedTabLabel = HIGClosableTabLabel diff --git a/zenmap/zenmapGUI/higwidgets/higprogressbars.py b/zenmap/zenmapGUI/higwidgets/higprogressbars.py new file mode 100644 index 0000000..edd2d1b --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higprogressbars.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higprogressbars.py + + progress bars classes +""" + +__all__ = ['HIGLabeledProgressBar'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from .higboxes import HIGHBox + + +class HIGLabeledProgressBar(HIGHBox): + def __init__(self, label=None): + HIGHBox.__init__(self) + if label: + self.label = HIGEntryLabel(label) + self.pack_label(self.label) + self.progress_bar = Gtk.ProgressBar() + self.progress_bar.set_size_request(80, 16) + self.pack_label(self.progress_bar) + + def show(self): + HIGHBox.show_all(self) diff --git a/zenmap/zenmapGUI/higwidgets/higscrollers.py b/zenmap/zenmapGUI/higwidgets/higscrollers.py new file mode 100644 index 0000000..83e6b16 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higscrollers.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higscrollers.py + + scrollers related classes +""" + +__all__ = ['HIGScrolledWindow'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +class HIGScrolledWindow(Gtk.ScrolledWindow): + def __init__(self): + Gtk.ScrolledWindow.__init__(self) + self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.set_border_width(5) diff --git a/zenmap/zenmapGUI/higwidgets/higspinner.py b/zenmap/zenmapGUI/higwidgets/higspinner.py new file mode 100644 index 0000000..977bfb5 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higspinner.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higspinner.py + + a pygtk spinner, based on the epiphany/nautilus implementation +""" + +__all__ = ['HIGSpinner'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib, Gdk, GdkPixbuf + + +class HIGSpinnerImages: + def __init__(self): + """This class holds list of GDK Pixbuffers. + + - static_pixbufs is used for multiple static pixbuffers + - self.animated_pixbufs is used for the pixbuffers that make up the + animation + """ + + dprint('HIGSpinnerImages::__init__') + + # The Nautilus/Epiphany implementation uses a single "rest/quiescent" + # static pixbuffer. We'd rather allow the developer to choose from + # multiple static states, such as "done" or "failed". + # Index it by a name like that. + self.static_pixbufs = {} + + # We should have a default rest pixbuf, set it with set_rest_pixbuf() + self.rest_pixbuf = None + + # This is a list of pixbufs to be used on the animation + # For now, we're only implementing a single animation. Inconsistent! + self.animated_pixbufs = [] + + def add_static_pixbuf(self, name, pixbuf, default_on_rest=False): + """Add a static pixbuf. + + If this is the first one, make it the default pixbuffer on rest. + The user can make some other pixbuf the new default on rest, by setting + default_on_rest to True. + """ + + dprint('HIGSpinnerImages::add_static_pixbuf') + + self.static_pixbufs[name] = pixbuf + if (len(self.static_pixbufs) == 1) or default_on_rest: + self.set_rest_pixbuf(name) + + def add_animated_pixbuf(self, pixbuf): + + dprint('HIGSpinnerImages::add_animated_pixbuf') + + self.animated_pixbufs.append(pixbuf) + + def set_rest_pixbuf(self, name): + """Sets the pixbuf that will be used on the default, 'rest' state. """ + + dprint('HIGSpinnerImages::set_rest_pixbuf') + + if name not in self.static_pixbufs: + raise StaticPixbufNotFound + + # self.rest_pixbuf holds the *real* pixbuf, not its name + self.rest_pixbuf = self.static_pixbufs[name] + + def set_size(self, width, height): + """Sets the size of each pixbuf (static and animated)""" + new_animated = [] + for p in self.animated_pixbufs: + new_animated.append(p.scale_simple(width, height, + GdkPixbuf.InterpType.BILINEAR)) + self.animated_pixbufs = new_animated + + for k in self.static_pixbufs: + self.static_pixbufs[k] = self.static_pixbufs[k].scale_simple( + width, height, GdkPixbuf.InterpType.BILINEAR) + + self.rest_pixbuf = self.rest_pixbuf.scale_simple( + width, height, GdkPixbuf.InterpType.BILINEAR) + + self.images_width = width + self.images_height = height + + +class HIGSpinnerCache: + """This hols a copy of the images used on the HIGSpinners instances.""" + def __init__(self): + + dprint('HIGSpinnerCache::__init__') + + # Our own instance of a HIGSpinnerImages + self.spinner_images = HIGSpinnerImages() + + # These are on Private member in the C implementation + self.icon_theme = Gtk.IconTheme() + self.originals = None + self.images = None + + # We might have access to a "default" animated icon. + # For example, if we're on a GNOME desktop, and have the (default) + # "gnome-icon-theme" package installed, we might have access + # to "gnome-spinner". Check it before using, though + if (self.icon_theme.lookup_icon("gnome-spinner", -1, 0)): + self.default_animated_icon_name = "gnome-spinner" + else: + self.default_animated_icon_name = None + + def load_animated_from_lookup(self, icon_name=None): + """Loads an animated icon by doing a lookup on the icon theme.""" + + # If user do not choose a icon_name, use the default one + if icon_name is None: + icon_name = self.default_animated_icon_name + + # Even the default one (now on icon_name) might not be available + if icon_name is None: + raise AnimatedIconNotFound + + # Try to lookup the icon + icon_info = self.icon_theme.lookup_icon(icon_name, -1, 0) + # Even if icon_name exists, it might not be found by lookup + if icon_info is None: + raise AnimatedIconNotFound + + # Base size is, according to PyGTK docs: + # "a size for the icon that was specified by the icon theme creator, + # This may be different than the actual size of image." + # Ouch! We are acting on blind faith here... + size = icon_info.get_base_size() + + # NOTE: If the icon is a builtin, it will not have a filename, see: + # http://www.pygtk.org/pygtk2reference/class-gtkicontheme.html + # But, we are not using the gtk.ICON_LOOKUP_USE_BUILTIN flag, nor does + # GTK+ has a builtin animation, so we are safe ;-) + filename = icon_info.get_filename() + + # Now that we have a filename, call load_animated_from_filename() + self.load_animated_from_filename(filename, size) + + def load_animated_from_filename(self, filename, size): + # grid_pixbuf is a pixbuf that holds the entire + grid_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + grid_width = grid_pixbuf.get_width() + grid_height = grid_pixbuf.get_height() + + for x in range(0, grid_width, size): + for y in range(0, grid_height, size): + self.spinner_images.add_animated_pixbuf( + self.__extract_frame(grid_pixbuf, x, y, size, size)) + + def load_static_from_lookup(self, icon_name="gnome-spinner-rest", + key_name=None): + icon_info = self.icon_theme.lookup_icon(icon_name, -1, 0) + filename = icon_info.get_filename() + + # Now that we have a filename, call load_static_from_filename() + self.load_static_from_filename(filename) + + def load_static_from_filename(self, filename, key_name=None): + icon_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + + if key_name is None: + key_name = filename.split(".")[0] + + self.spinner_images.add_static_pixbuf(key_name, icon_pixbuf) + + def __extract_frame(self, pixbuf, x, y, w, h): + """Cuts a sub pixbuffer, usually a frame of an animation. + + - pixbuf is the complete pixbuf, from which a frame will be cut off + - x/y are the position + - w (width) is the is the number of pixels to move right + - h (height) is the is the number of pixels to move down + """ + if (x + w > pixbuf.get_width()) or (y + h > pixbuf.get_height()): + raise PixbufSmallerThanRequiredError + return pixbuf.subpixbuf(x, y, w, h) + + def _write_animated_pixbuf_to_files(self, path_format, image_format): + """Writes image files from self.spinner_images.animated_pixbufs + + - path_format should be a format string with one occurrence of a + string substitution, such as '/tmp/animation_%s.png' + - image_format can be either 'png' or 'jpeg' + """ + counter = 0 + for i in self.spinner_images.animated_pixbufs: + i.save(path_format % counter, "png") + counter += 1 + + def _write_static_pixbuf_to_file(self, key_name, path_name, image_format): + self.spinner_images.static_pixbufs[key_name].save(path_name, + image_format) + + +class HIGSpinner(Gtk.EventBox): + """Simple spinner, such as the one found in webbrowsers and file managers. + + You can construct it with the optional parameters: + * images, a list of images that will make up the animation + * width, the width that will be set for the images + * height, the height that will be set for the images + """ + + #__gsignals__ = {'expose-event': 'override', + # 'size-request': 'override'} + + def __init__(self): + Gtk.EventBox.__init__(self) + + #self.set_events(self.get_events()) + + # This holds a GDK Graphic Context + self.gc = None + + # These are sane defaults, but should really come from the images + self.images_width = 32 + self.images_height = 32 + + # Timeout set to 100 milliseconds per frame, just as the + # Nautilus/Epiphany implementation + self.timeout = 120 + + # Initialize a cache for ourselves + self.cache = HIGSpinnerCache() + self.cache.load_static_from_lookup() + self.cache.load_animated_from_lookup() + + # timer_task it the gobject.timeout_add identifier (when the animation + # is in progress, and __bump_frame is being continually called). If the + # spinner is static, timer_task is 0 + self.timer_task = 0 + # animated_pixbuf_index is a index on + self.animated_pixbuf_index = 0 + # current_pixbuf is initially the default rest_pixbuf + self.current_pixbuf = self.cache.spinner_images.rest_pixbuf + + def __bump_frame(self): + """This function moves the animated frame to the next one, or, if it's + currently the last one, back to the first one""" + animated_list = self.cache.spinner_images.animated_pixbufs + if self.animated_pixbuf_index == (len(animated_list) - 1): + # back to the first one + self.animated_pixbuf_index = 0 + else: + # go the next one + self.animated_pixbuf_index += 1 + + self.queue_draw() + return True + + def __select_pixbuf(self): + """This selects either a rest pixbuf or a animation frame based on the + status of timer_task.""" + if self.timer_task == 0: + self.current_pixbuf = self.cache.spinner_images.rest_pixbuf + else: + self.current_pixbuf = self.cache.spinner_images.animated_pixbufs[ + self.animated_pixbuf_index] + + def start(self): + """Starts the animation""" + if self.timer_task == 0: + self.timer_task = GLib.timeout_add(self.timeout, + self.__bump_frame) + + def pause(self): + """Pauses the animation""" + if self.timer_task != 0: + GLib.source_remove(self.timer_task) + + self.timer_task = 0 + self.queue_draw() + + def stop(self): + """Stops the animation + + Do the same stuff as pause, but returns the animation to the + beginning.""" + self.pause() + self.animated_pixbuf_index = 0 + + def set_speed(speed_in_milliseconds): + self.timeout = speed_in_milliseconds + self.pause() + self.start() + + def do_expose_event(self, event): + #self.chain(event) + + if self.cache.spinner_images.rest_pixbuf is None: + raise RestPixbufNotFound + + self.__select_pixbuf() + + width = self.current_pixbuf.get_width() + height = self.current_pixbuf.get_height() + x_offset = (self.allocation.width - width) // 2 + y_offset = (self.allocation.height - height) // 2 + + pix_area = Gdk.Rectangle(x_offset + self.allocation.x, + y_offset + self.allocation.y, + width, height) + + dest = event.area.intersect(pix_area) + +# # If a graphic context doesn't not exist yet, create one +# if self.gc is None: +# self.gc = gtk.gdk.GC(self.window) +# #gc = self.gc +# +# cairo = self.window.cairo_create() +# +# +# self.window.draw_pixbuf(self.gc, +# self.current_pixbuf, +# dest.x - x_offset - self.allocation.x, +# dest.y - y_offset - self.allocation.y, +# dest.x, dest.y, +# dest.width, dest.height) + + def do_size_request(self, requisition): + # http://www.pygtk.org/pygtk2reference/class-gtkrequisition.html + + # FIXME, this should really come from the pixbuf size + margins + requisition.width = self.cache.spinner_images.images_width + requisition.height = self.cache.spinner_images.images_height diff --git a/zenmap/zenmapGUI/higwidgets/higtables.py b/zenmap/zenmapGUI/higwidgets/higtables.py new file mode 100644 index 0000000..0337cf6 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higtables.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higlogindialog.py + + a basic login/authentication dialog +""" + +__all__ = ['HIGTable'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +#from higlabels import * +#from higentries import * + + +class HIGTable(Gtk.Table): + """ + A HIGFied table + """ + + # TODO: + # - Automatic position packing, + # - Generic attach function that detects the widget type + + def __init__(self, rows=1, columns=1, homogeneous=False): + Gtk.Table.__init__(self, n_rows=rows, n_columns=columns, homogeneous=homogeneous) + self.set_row_spacings(6) + self.set_col_spacings(12) + + self.rows = rows + self.columns = columns + + def attach_label(self, widget, x0, x, y0, y): + self.attach(widget, x0, x, y0, y, xoptions=Gtk.AttachOptions.FILL) + + def attach_entry(self, widget, x0, x, y0, y): + self.attach(widget, x0, x, y0, y, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND) diff --git a/zenmap/zenmapGUI/higwidgets/higtextviewers.py b/zenmap/zenmapGUI/higwidgets/higtextviewers.py new file mode 100644 index 0000000..1b2d2f0 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higtextviewers.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higtextviewers.py + + text viewers related classes +""" + +__all__ = ['HIGTextView'] + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +class HIGTextView(Gtk.TextView): + def __init__(self, text=''): + Gtk.TextView.__init__(self) + self.set_wrap_mode(Gtk.WrapMode.WORD) + self.get_buffer().set_text(text) diff --git a/zenmap/zenmapGUI/higwidgets/higwindows.py b/zenmap/zenmapGUI/higwidgets/higwindows.py new file mode 100644 index 0000000..0ec4df3 --- /dev/null +++ b/zenmap/zenmapGUI/higwidgets/higwindows.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +# ***********************IMPORTANT NMAP LICENSE TERMS************************ +# * +# * The Nmap Security Scanner is (C) 1996-2023 Nmap Software LLC ("The Nmap +# * Project"). Nmap is also a registered trademark of the Nmap Project. +# * +# * This program is distributed under the terms of the Nmap Public Source +# * License (NPSL). The exact license text applying to a particular Nmap +# * release or source code control revision is contained in the LICENSE +# * file distributed with that version of Nmap or source code control +# * revision. More Nmap copyright/legal information is available from +# * https://nmap.org/book/man-legal.html, and further information on the +# * NPSL license itself can be found at https://nmap.org/npsl/ . This +# * header summarizes some key points from the Nmap license, but is no +# * substitute for the actual license text. +# * +# * Nmap is generally free for end users to download and use themselves, +# * including commercial use. It is available from https://nmap.org. +# * +# * The Nmap license generally prohibits companies from using and +# * redistributing Nmap in commercial products, but we sell a special Nmap +# * OEM Edition with a more permissive license and special features for +# * this purpose. See https://nmap.org/oem/ +# * +# * If you have received a written Nmap license agreement or contract +# * stating terms other than these (such as an Nmap OEM license), you may +# * choose to use and redistribute Nmap under those terms instead. +# * +# * The official Nmap Windows builds include the Npcap software +# * (https://npcap.com) for packet capture and transmission. It is under +# * separate license terms which forbid redistribution without special +# * permission. So the official Nmap Windows builds may not be redistributed +# * without special permission (such as an Nmap OEM license). +# * +# * Source is provided to this software because we believe users have a +# * right to know exactly what a program is going to do before they run it. +# * This also allows you to audit the software for security holes. +# * +# * Source code also allows you to port Nmap to new platforms, fix bugs, and add +# * new features. You are highly encouraged to submit your changes as a Github PR +# * or by email to the dev@nmap.org mailing list for possible incorporation into +# * the main distribution. Unless you specify otherwise, it is understood that +# * you are offering us very broad rights to use your submissions as described in +# * the Nmap Public Source License Contributor Agreement. This is important +# * because we fund the project by selling licenses with various terms, and also +# * because the inability to relicense code has caused devastating problems for +# * other Free Software projects (such as KDE and NASM). +# * +# * The free version of Nmap is distributed in the hope that it will be +# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties, +# * indemnification and commercial support are all available through the +# * Npcap OEM program--see https://nmap.org/oem/ +# * +# ***************************************************************************/ + +""" +higwidgets/higwindows.py + + window related classes +""" + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +__all__ = ('HIGWindow', 'HIGMainWindow') + + +class HIGWindow(Gtk.Window): + """HIGFied Window""" + def __init__(self, type=Gtk.WindowType.TOPLEVEL): + Gtk.Window.__init__(self, type=type) + self.set_border_width(5) + +# The Application main window should have no borders... +# so it should be really a gtk.Window +HIGMainWindow = Gtk.Window |