summaryrefslogtreecommitdiffstats
path: root/zenmap/zenmapGUI
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--zenmap/zenmapGUI/About.py366
-rw-r--r--zenmap/zenmapGUI/App.py325
-rw-r--r--zenmap/zenmapGUI/BugReport.py147
-rw-r--r--zenmap/zenmapGUI/CrashReport.py176
-rw-r--r--zenmap/zenmapGUI/DiffCompare.py456
-rw-r--r--zenmap/zenmapGUI/FileChoosers.py245
-rw-r--r--zenmap/zenmapGUI/FilterBar.py77
-rw-r--r--zenmap/zenmapGUI/Icons.py206
-rw-r--r--zenmap/zenmapGUI/MainWindow.py919
-rw-r--r--zenmap/zenmapGUI/NmapOutputProperties.py322
-rw-r--r--zenmap/zenmapGUI/NmapOutputViewer.py320
-rw-r--r--zenmap/zenmapGUI/OptionBuilder.py498
-rw-r--r--zenmap/zenmapGUI/Print.py135
-rw-r--r--zenmap/zenmapGUI/ProfileCombo.py108
-rw-r--r--zenmap/zenmapGUI/ProfileEditor.py403
-rw-r--r--zenmap/zenmapGUI/ProfileHelp.py91
-rw-r--r--zenmap/zenmapGUI/ScanHostDetailsPage.py470
-rw-r--r--zenmap/zenmapGUI/ScanHostsView.py286
-rw-r--r--zenmap/zenmapGUI/ScanInterface.py962
-rw-r--r--zenmap/zenmapGUI/ScanNmapOutputPage.py239
-rw-r--r--zenmap/zenmapGUI/ScanOpenPortsPage.py455
-rw-r--r--zenmap/zenmapGUI/ScanRunDetailsPage.py264
-rw-r--r--zenmap/zenmapGUI/ScanScanListPage.py173
-rw-r--r--zenmap/zenmapGUI/ScanToolbar.py190
-rw-r--r--zenmap/zenmapGUI/ScansListStore.py161
-rw-r--r--zenmap/zenmapGUI/ScriptInterface.py706
-rw-r--r--zenmap/zenmapGUI/SearchGUI.py915
-rw-r--r--zenmap/zenmapGUI/SearchWindow.py183
-rw-r--r--zenmap/zenmapGUI/TargetCombo.py103
-rw-r--r--zenmap/zenmapGUI/TopologyPage.py162
-rw-r--r--zenmap/zenmapGUI/__init__.py56
-rw-r--r--zenmap/zenmapGUI/higwidgets/__init__.py81
-rw-r--r--zenmap/zenmapGUI/higwidgets/gtkutils.py95
-rw-r--r--zenmap/zenmapGUI/higwidgets/higboxes.py120
-rw-r--r--zenmap/zenmapGUI/higwidgets/higbuttons.py117
-rw-r--r--zenmap/zenmapGUI/higwidgets/higdialogs.py140
-rw-r--r--zenmap/zenmapGUI/higwidgets/higentries.py80
-rw-r--r--zenmap/zenmapGUI/higwidgets/higexpanders.py88
-rw-r--r--zenmap/zenmapGUI/higwidgets/higframe.py105
-rw-r--r--zenmap/zenmapGUI/higwidgets/higlabels.py180
-rw-r--r--zenmap/zenmapGUI/higwidgets/higlogindialogs.py116
-rw-r--r--zenmap/zenmapGUI/higwidgets/hignotebooks.py128
-rw-r--r--zenmap/zenmapGUI/higwidgets/higprogressbars.py85
-rw-r--r--zenmap/zenmapGUI/higwidgets/higscrollers.py76
-rw-r--r--zenmap/zenmapGUI/higwidgets/higspinner.py388
-rw-r--r--zenmap/zenmapGUI/higwidgets/higtables.py96
-rw-r--r--zenmap/zenmapGUI/higwidgets/higtextviewers.py76
-rw-r--r--zenmap/zenmapGUI/higwidgets/higwindows.py80
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>&lt;dev@nmap.org&gt;</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