summaryrefslogtreecommitdiffstats
path: root/zenmap/zenmapCore
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--zenmap/zenmapCore/BasePaths.py79
-rw-r--r--zenmap/zenmapCore/DelayedObject.py77
-rw-r--r--zenmap/zenmapCore/Diff.py187
-rw-r--r--zenmap/zenmapCore/I18N.py109
-rw-r--r--zenmap/zenmapCore/NSEDocParser.py124
-rw-r--r--zenmap/zenmapCore/Name.py73
-rw-r--r--zenmap/zenmapCore/NetworkInventory.py675
-rw-r--r--zenmap/zenmapCore/NmapCommand.py269
-rw-r--r--zenmap/zenmapCore/NmapOptions.py1406
-rw-r--r--zenmap/zenmapCore/NmapParser.py1345
-rw-r--r--zenmap/zenmapCore/Paths.py243
-rw-r--r--zenmap/zenmapCore/RecentScans.py116
-rw-r--r--zenmap/zenmapCore/ScriptArgsParser.py206
-rw-r--r--zenmap/zenmapCore/ScriptMetadata.py451
-rw-r--r--zenmap/zenmapCore/SearchResult.py546
-rw-r--r--zenmap/zenmapCore/StringPool.py80
-rw-r--r--zenmap/zenmapCore/TargetList.py117
-rw-r--r--zenmap/zenmapCore/UmitConf.py643
-rw-r--r--zenmap/zenmapCore/UmitConfigParser.py137
-rw-r--r--zenmap/zenmapCore/UmitDB.py324
-rw-r--r--zenmap/zenmapCore/UmitLogging.py97
-rw-r--r--zenmap/zenmapCore/UmitOptionParser.py202
-rw-r--r--zenmap/zenmapCore/Version.py1
-rw-r--r--zenmap/zenmapCore/__init__.py56
24 files changed, 7563 insertions, 0 deletions
diff --git a/zenmap/zenmapCore/BasePaths.py b/zenmap/zenmapCore/BasePaths.py
new file mode 100644
index 0000000..13be7f3
--- /dev/null
+++ b/zenmap/zenmapCore/BasePaths.py
@@ -0,0 +1,79 @@
+#!/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 os.path
+import sys
+
+from zenmapCore.Name import APP_NAME
+
+
+HOME = os.path.expanduser("~")
+
+# The base_paths dict in this file gives symbolic names to various files. For
+# example, use base_paths.target_list instead of 'target_list.txt'.
+
+base_paths = dict(user_config_file=APP_NAME + '.conf',
+ user_config_dir=os.path.join(HOME, '.' + APP_NAME),
+ scan_profile='scan_profile.usp',
+ profile_editor='profile_editor.xml',
+ recent_scans='recent_scans.txt',
+ target_list='target_list.txt',
+ options='options.xml',
+ user_home=HOME,
+ db=APP_NAME + ".db",
+ version=APP_NAME + "_version")
diff --git a/zenmap/zenmapCore/DelayedObject.py b/zenmap/zenmapCore/DelayedObject.py
new file mode 100644
index 0000000..d6751b7
--- /dev/null
+++ b/zenmap/zenmapCore/DelayedObject.py
@@ -0,0 +1,77 @@
+#!/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/
+# *
+# ***************************************************************************/
+
+
+class DelayedObject(object):
+ def __init__(self, klass, *args, **kwargs):
+ object.__setattr__(self, "klass", klass)
+ object.__setattr__(self, "args", args)
+ object.__setattr__(self, "kwargs", kwargs)
+
+ def __setattr__(self, name, value):
+ self = object.__getattribute__(self, "klass")(
+ *object.__getattribute__(self, "args"),
+ **object.__getattribute__(self, "kwargs")
+ )
+ setattr(self, name, value)
+
+ def __getattribute__(self, name):
+ self = object.__getattribute__(self, "klass")(
+ *object.__getattribute__(self, "args"),
+ **object.__getattribute__(self, "kwargs")
+ )
+ return getattr(self, name)
diff --git a/zenmap/zenmapCore/Diff.py b/zenmap/zenmapCore/Diff.py
new file mode 100644
index 0000000..44f1271
--- /dev/null
+++ b/zenmap/zenmapCore/Diff.py
@@ -0,0 +1,187 @@
+#!/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 subprocess
+import sys
+import tempfile
+# Prevent loading PyXML
+import xml
+xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x]
+
+import xml.sax
+
+from zenmapCore.Name import APP_NAME
+from zenmapCore.NmapParser import NmapParserSAX
+from zenmapCore.UmitConf import PathsConfig
+from zenmapCore.UmitLogging import log
+import zenmapCore.Paths
+
+# The [paths] configuration from zenmap.conf, used to get ndiff_command_path.
+paths_config = PathsConfig()
+
+
+class NdiffParseException(Exception):
+ pass
+
+
+def get_path():
+ """Return a value for the PATH environment variable that is appropriate
+ for the current platform. It will be the PATH from the environment plus
+ possibly some platform-specific directories."""
+ path_env = os.getenv("PATH")
+ if path_env is None:
+ search_paths = []
+ else:
+ search_paths = path_env.split(os.pathsep)
+ for path in zenmapCore.Paths.get_extra_executable_search_paths():
+ if path not in search_paths:
+ search_paths.append(path)
+ return os.pathsep.join(search_paths)
+
+
+class NdiffCommand(subprocess.Popen):
+ def __init__(self, filename_a, filename_b, temporary_filenames=[]):
+ self.temporary_filenames = temporary_filenames
+
+ search_paths = get_path()
+ env = dict(os.environ)
+ env["PATH"] = search_paths
+ if "Zenmap.app" in sys.executable:
+ # These vars are set by the launcher, but they can interfere with
+ # Ndiff because Ndiff is also a Python application. Without
+ # removing these, Ndiff will attempt to run using the
+ # bundled Python library, and may run into version or
+ # architecture mismatches.
+ if "PYTHONPATH" in env:
+ del env["PYTHONPATH"]
+ if "PYTHONHOME" in env:
+ del env["PYTHONHOME"]
+
+ command_list = [
+ paths_config.ndiff_command_path,
+ "--verbose",
+ "--",
+ filename_a,
+ filename_b
+ ]
+ self.stdout_file = tempfile.TemporaryFile(
+ mode="r",
+ prefix=APP_NAME + "-ndiff-",
+ suffix=".xml"
+ )
+
+ log.debug("Running command: %s" % repr(command_list))
+ # shell argument explained in zenmapCore.NmapCommand.py
+ subprocess.Popen.__init__(
+ self,
+ command_list,
+ universal_newlines=True,
+ stdout=self.stdout_file,
+ stderr=self.stdout_file,
+ env=env,
+ shell=(sys.platform == "win32")
+ )
+
+ def get_scan_diff(self):
+ self.wait()
+ self.stdout_file.seek(0)
+
+ return self.stdout_file.read()
+
+ def close(self):
+ """Clean up temporary files."""
+ self.stdout_file.close()
+ for filename in self.temporary_filenames:
+ log.debug("Remove temporary diff file %s." % filename)
+ os.remove(filename)
+ self.temporary_filenames = []
+
+ def kill(self):
+ self.close()
+
+
+def ndiff(scan_a, scan_b):
+ """Run Ndiff on two scan results, which may be filenames or NmapParserSAX
+ objects, and return a running NdiffCommand object."""
+ temporary_filenames = []
+
+ if isinstance(scan_a, NmapParserSAX):
+ fd, filename_a = tempfile.mkstemp(
+ prefix=APP_NAME + "-diff-",
+ suffix=".xml"
+ )
+ temporary_filenames.append(filename_a)
+ f = os.fdopen(fd, "w")
+ scan_a.write_xml(f)
+ f.close()
+ else:
+ filename_a = scan_a
+
+ if isinstance(scan_b, NmapParserSAX):
+ fd, filename_b = tempfile.mkstemp(
+ prefix=APP_NAME + "-diff-",
+ suffix=".xml"
+ )
+ temporary_filenames.append(filename_b)
+ f = os.fdopen(fd, "w")
+ scan_b.write_xml(f)
+ f.close()
+ else:
+ filename_b = scan_b
+
+ return NdiffCommand(filename_a, filename_b, temporary_filenames)
diff --git a/zenmap/zenmapCore/I18N.py b/zenmap/zenmapCore/I18N.py
new file mode 100644
index 0000000..0942a1f
--- /dev/null
+++ b/zenmap/zenmapCore/I18N.py
@@ -0,0 +1,109 @@
+#!/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 locale
+import os
+
+from zenmapCore.Name import APP_NAME
+
+
+def get_locales():
+ """Get a list of locales to use based on system configuration."""
+ locales = []
+ # locale.getdefaultlocales already looks at LANG et al. on Unix but not on
+ # Windows. We look at the environment variables first to allow overriding
+ # the system-wide setting on Windows.
+ for envar in ("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"):
+ val = os.environ.get(envar)
+ if val:
+ locales = val.split(":")
+ break
+ try:
+ loc, enc = locale.getdefaultlocale()
+ if loc is not None:
+ locales.append(loc)
+ except ValueError:
+ # locale.getdefaultlocale can fail with ValueError on certain locale
+ # names; it has been seen with at least en_NG.
+ # http://bugs.python.org/issue6895
+ pass
+ return locales
+
+
+def install_gettext(locale_dir):
+ try:
+ locale.setlocale(locale.LC_ALL, '')
+ except locale.Error:
+ # This can happen if the LANG environment variable is set to something
+ # invalid, like LANG=nothing or LANG=en_US/utf8 or LANG=us-ascii.
+ # Continue without internationalization.
+ pass
+
+ try:
+ import gettext
+ except ImportError:
+ pass
+ else:
+ t = gettext.translation(
+ APP_NAME, locale_dir, languages=get_locales(), fallback=True)
+ t.install()
+
+# Install a dummy _ function so modules can safely use it after importing this
+# module, even if they don't install the gettext version.
+
+import builtins
+builtins.__dict__["_"] = lambda s: s
diff --git a/zenmap/zenmapCore/NSEDocParser.py b/zenmap/zenmapCore/NSEDocParser.py
new file mode 100644
index 0000000..33b0af9
--- /dev/null
+++ b/zenmap/zenmapCore/NSEDocParser.py
@@ -0,0 +1,124 @@
+#!/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 re
+
+
+class NSEDocEvent (object):
+ def __init__(self, type, text=None):
+ self.type = type
+ self.text = text
+
+
+def nsedoc_parse_sub(text, pos):
+ """Parse paragraph-level NSEDoc markup, inside of paragraphs and lists.
+ Returns the position after the end of parsing followed by a list of
+ events."""
+ events = []
+ m = re.match(r'([^\n]*?)<code>(.*?)</code>', text[pos:], re.S)
+ if m:
+ if m.group(1):
+ events.append(NSEDocEvent("text", m.group(1).replace("\n", " ")))
+ events.append(NSEDocEvent("code", m.group(2)))
+ return pos + m.end(), events
+ m = re.match(r'[^\n]*(\n|$)', text[pos:])
+ if m:
+ if m.group():
+ events.append(NSEDocEvent("text", m.group().replace("\n", " ")))
+ return pos + m.end(), events
+ return pos, events
+
+
+def nsedoc_parse(text):
+ """Parse text marked up for NSEDoc. This is a generator that returns a
+ sequence of NSEDocEvents. The type of the event may be "paragraph_start",
+ "paragraph_end", "list_start", "list_end", "list_item_start",
+ "list_item_end", "text", or "code". The types "text" and "code" have a text
+ member with the text that they contain."""
+ i = 0
+ in_list = False
+
+ while i < len(text):
+ while i < len(text) and text[i].isspace():
+ i += 1
+ if i >= len(text):
+ break
+ yield NSEDocEvent("paragraph_start")
+ while i < len(text):
+ if re.match(r'\s*(\n|$)', text[i:]):
+ break
+ if text.startswith("* ", i):
+ if not in_list:
+ yield NSEDocEvent("list_start")
+ in_list = True
+ i += 2
+ yield NSEDocEvent("list_item_start")
+ i, events = nsedoc_parse_sub(text, i)
+ for event in events:
+ yield event
+ yield NSEDocEvent("list_item_end")
+ else:
+ if in_list:
+ yield NSEDocEvent("list_end")
+ in_list = False
+ i, events = nsedoc_parse_sub(text, i)
+ for event in events:
+ yield event
+ if in_list:
+ yield NSEDocEvent("list_end")
+ in_list = False
+ yield NSEDocEvent("paragraph_end")
diff --git a/zenmap/zenmapCore/Name.py b/zenmap/zenmapCore/Name.py
new file mode 100644
index 0000000..166e7fe
--- /dev/null
+++ b/zenmap/zenmapCore/Name.py
@@ -0,0 +1,73 @@
+#!/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 file contains global definitions of program names. The plain names are
+# the usually lower-case program or package names. The display names are
+# properly capitalized for use in human-readable sentences.
+
+APP_NAME = "zenmap"
+APP_DISPLAY_NAME = "Zenmap"
+APP_WEB_SITE = "https://nmap.org/zenmap/"
+APP_DOWNLOAD_SITE = "https://nmap.org/download.html"
+APP_DOCUMENTATION_SITE = "https://nmap.org/book/zenmap.html"
+APP_COPYRIGHT = "Copyright 2005-2023 Nmap Software LLC"
+
+NMAP_DISPLAY_NAME = "Nmap"
+NMAP_WEB_SITE = "https://nmap.org"
+
+UMIT_DISPLAY_NAME = "Umit"
+UMIT_WEB_SITE = "http://www.umitproject.org/"
diff --git a/zenmap/zenmapCore/NetworkInventory.py b/zenmap/zenmapCore/NetworkInventory.py
new file mode 100644
index 0000000..f66000d
--- /dev/null
+++ b/zenmap/zenmapCore/NetworkInventory.py
@@ -0,0 +1,675 @@
+#!/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 unittest
+import zenmapCore
+import zenmapCore.NmapParser
+from zenmapGUI.SearchGUI import SearchParser
+from .SearchResult import HostSearch
+
+
+class NetworkInventory(object):
+ """This class acts as a container for aggregated scans. It is also
+ responsible for opening/saving the aggregation from/to persistent
+ storage."""
+ def __init__(self, filename=None):
+ # A list of all scans that make up this inventory
+ self.scans = []
+
+ # A dictionary mapping parsed scans to filenames they were loaded from
+ self.filenames = {}
+
+ # A dictionary mapping IP addresses into HostInfo objects
+ self.hosts = {}
+
+ if filename is not None:
+ self.open_from_file(filename)
+
+ def add_scan(self, scan, filename=None):
+ """Adds a scan to the list of scans. The object passed as an argument
+ should be a parsed nmap result."""
+ from time import localtime
+
+ for host in scan.get_hosts():
+ addr = ""
+ if host.ipv6 is not None:
+ # This is an IPv6 host, so we add the IPv6 address to the map
+ addr = host.ipv6["addr"]
+ elif host.ip is not None:
+ # IPv4
+ addr = host.ip["addr"]
+
+ if addr not in self.hosts:
+ # Add this host to the hosts dictionary, mapped by IP address
+ self.hosts[addr] = host.make_clone()
+ else:
+ # This host is already present in the host list, so we need to
+ # update its info with the info held in the current host object
+ old_host = self.hosts[addr]
+ # We need to find old_host's scan date
+ old_date = localtime(0)
+ for old_scan in self.scans:
+ if old_host in old_scan.get_hosts():
+ old_date = old_scan.get_date()
+ new_date = scan.get_date()
+ self._update_host_info(
+ old_host, host, old_date, new_date, scan)
+
+ self.scans.append(scan)
+
+ if filename is not None:
+ basename = os.path.basename(filename)
+
+ if basename in self.filenames.values():
+ # We need to generate a new filename, since this basename
+ # already exists
+ base = basename
+ ext = "xml"
+ try:
+ base, ext = basename.rsplit(".", 1)
+ except ValueError:
+ pass
+
+ counter = 2
+ while basename in self.filenames.values():
+ basename = "%s %s.%s" % (base, counter, ext)
+ counter += 1
+
+ self.filenames[scan] = basename
+
+ def remove_scan(self, scan):
+ """Removes a scan and any host information it contained from the
+ inventory."""
+ # Note: If a scan is passed in that isn't in the inventory then this
+ # method will throw a ValueError Exception and will not finish
+ # Remove the scan from our scan list
+ self.scans.remove(scan)
+
+ # Clear the host dictionary
+ self.hosts = {}
+
+ # Remember the scan list
+ scans = self.scans
+
+ # Empty it
+ self.scans = []
+
+ # Delete the filename entry, if any
+ if scan in self.filenames:
+ del self.filenames[scan]
+
+ # For each scan in the remembered list, append it to the scan list and
+ # update the host list accordingly
+ for scan in scans:
+ self.add_scan(scan)
+
+ def _update_host_info(self, old_host, new_host,
+ old_date, new_date, new_scan):
+ """This function is called when a host needs to be added to the hosts
+ dictionary, but another HostInfo object for that host already exists
+ in the dictionary (from a previous scan). In that case, we need to
+ update the original HostInfo object so that it holds information from
+ both scans."""
+
+ # Ports
+ old_list = []
+ old_list.extend(old_host.ports)
+ for new_port in new_host.ports:
+ # Check if new_port is already present in old_host's ports
+ for old_port in old_host.ports:
+ if (old_port["portid"] == new_port["portid"] and
+ old_port["protocol"] == new_port["protocol"]):
+ old_list.remove(old_port)
+ # We update old_host's port information to reflect the
+ # latest known port state
+ if old_date < new_date:
+ index = old_host.ports.index(old_port)
+ old_host.ports[index] = new_port
+ # Finished processing this new_port, we jump to the next
+ break
+ else:
+ # This new_port isn't present in old_host, so we simply append
+ # it to old_host's port info
+ old_host.ports.append(new_port)
+
+ ports = new_scan.get_port_protocol_dict()
+
+ #remove ports which are no longer up
+ if old_date < new_date:
+ for defunct_port in old_list:
+ # Check if defunct_port is in ports
+ # and that the protocol matches
+ port_number = int(defunct_port['portid'])
+ if port_number in ports:
+ if defunct_port['protocol'] in ports[port_number]:
+ old_host.ports.remove(defunct_port)
+
+ # extraports, ipidsequence, state, tcpsequence, tcptssequence, uptime
+ if old_date < new_date:
+ old_host.extraports = new_host.extraports
+ old_host.ipidsequence = new_host.ipidsequence
+ old_host.state = new_host.state
+ old_host.tcpsequence = new_host.tcpsequence
+ old_host.tcptssequence = new_host.tcptssequence
+ old_host.uptime = new_host.uptime
+
+ # Comment
+ if old_host.comment == "":
+ old_host.comment = new_host.comment
+ elif new_host.comment != "":
+ old_host.comment = "%s\n\n%s" % (
+ old_host.comment, new_host.comment)
+
+ # Hostnames
+ # Replace old_host's hostname with new_host's if old_host has no
+ # hostname or new_host's is newer.
+ if len(new_host.hostnames) > 0 and \
+ (len(old_host.hostnames) == 0 or old_date < new_date):
+ old_host.hostnames = new_host.hostnames
+
+ # MAC address
+ # If there was no MAC address set in old_host, set it to whatever is in
+ # new_host.mac. Do the same if both hosts have a MAC address set, but
+ # new_host's address is newer.
+ if (old_host.mac is None or
+ (old_host.mac is not None and
+ new_host.mac is not None and
+ old_date < new_date)
+ ):
+ old_host.mac = new_host.mac
+
+ # OS detection fields
+ # Replace old_host's OS detection fields with new_host's if old_host
+ # has no OS detection info or new_host's info is newer.
+ if (len(new_host.osmatches) > 0 and
+ (len(old_host.osmatches) == 0 or old_date < new_date)
+ ):
+ old_host.osmatches = new_host.osmatches
+ old_host.ports_used = new_host.ports_used
+
+ # Traceroute information
+ if (len(new_host.trace) > 0 and
+ (len(old_host.trace) == 0 or old_date < new_date)
+ ):
+ old_host.trace = new_host.trace
+
+ def get_scans(self):
+ return self.scans
+
+ def get_hosts(self):
+ return list(self.hosts.values())
+
+ def get_hosts_up(self):
+ return [h for h in list(self.hosts.values()) if h.get_state() == 'up']
+
+ def get_hosts_down(self):
+ return [h for h in list(self.hosts.values()) if h.get_state() == 'down']
+
+ def open_from_file(self, path):
+ """Loads a scan from the given file."""
+ from zenmapCore.NmapParser import NmapParser
+
+ parsed = NmapParser()
+ parsed.parse_file(path)
+ self.add_scan(parsed, path)
+
+ def open_from_dir(self, path):
+ """Loads all scans from the given directory into the network
+ inventory."""
+ from zenmapCore.NmapParser import NmapParser
+
+ for filename in os.listdir(path):
+ fullpath = os.path.join(path, filename)
+ if os.path.isdir(fullpath):
+ continue
+ parsed = NmapParser()
+ parsed.parse_file(fullpath)
+ self.add_scan(parsed, filename=fullpath)
+
+ def save_to_file(self, path, index, format="xml"):
+ """Saves the scan with the given list index into a file with a given
+ path. With format = "xml", saves Nmap XML; otherwise saves plain text
+ output."""
+ f = open(path, 'w')
+ if format == "xml":
+ self.get_scans()[index].write_xml(f)
+ self.filenames[self.get_scans()[index]] = f
+ else:
+ self.get_scans()[index].write_text(f)
+ f.close()
+
+ def _generate_filenames(self, path):
+ """Generates filenames for all scans that don't already have a
+ filename."""
+ # The directory must not contain filenames other than those in the
+ # self.filenames dictionary
+ for filename in os.listdir(path):
+ if os.path.basename(filename) not in self.filenames.values():
+ raise Exception("The destination directory contains a file"
+ "(%s) that's not a part of the current inventory."
+ "The inventory will not be saved." %
+ os.path.basename(filename))
+
+ for scan in self.scans:
+ if scan in self.filenames:
+ # This scan already has a filename
+ continue
+
+ date = "%04d%02d%02d%02d%02d" % (scan.date[0], scan.date[1],
+ scan.date[2], scan.date[3], scan.date[4])
+ filename = scan.get_scan_name()
+
+ # Prepend the date
+ filename = "%s %s" % (date, filename)
+
+ # Sanitize the filename
+ for char in ["\"", "'", "/", "\\", "?", "*", ":", ";"]:
+ if char in filename:
+ filename = filename.replace(char, "_")
+
+ # Filename length check
+ # https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits
+ if len(filename) > 250:
+ filename = filename[:250]
+
+ # TODO: Filename security checks?
+
+ # Try to open the file in append mode. If file.tell() returns a
+ # greater-than-zero value, this means that the file already exists
+ # and has some data in it, so we choose another filename until we
+ # successfully open a zero-length file.
+ filename_full = filename + ".xml"
+ counter = 2
+ while filename_full in self.filenames.values():
+ # There's already a scan with this filename, so we generate a
+ # new name by appending the counter value before the file
+ # extension.
+ filename_full = "%s %s.xml" % (filename, str(counter))
+ counter += 1
+
+ # Add the filename to the list of saved filenames
+ self.filenames[scan] = filename_full
+
+ def save_to_dir(self, path):
+ """Saves all scans in the inventory into a given directory and returns
+ a list of (full-path) filenames that were used to save the scans."""
+ self._generate_filenames(path)
+
+ for scan, filename in self.filenames.items():
+ f = open(os.path.join(path, filename), "w")
+ scan.write_xml(f)
+ f.close()
+
+ return self.filenames.values()
+
+ def open_from_db(self, id):
+ pass
+
+ def save_to_db(self):
+ # For now, this saves each scan making up the inventory separately in
+ # the database.
+ from time import time
+ from io import StringIO
+ from zenmapCore.UmitDB import Scans
+
+ for parsed in self.get_scans():
+ f = StringIO()
+ parsed.write_xml(f)
+
+ scan = Scans(scan_name=parsed.scan_name,
+ nmap_xml_output=f.getvalue(),
+ date=time())
+
+
+class FilteredNetworkInventory(NetworkInventory):
+ def __init__(self, filename=None):
+ NetworkInventory.__init__(self, filename)
+
+ # A dictionary listing host filtering criteria
+ self.search_dict = {}
+ self.filtered_hosts = []
+ search_keywords = dict()
+ search_keywords["target"] = "target"
+ search_keywords["t"] = "target"
+ search_keywords["inroute"] = "in_route"
+ search_keywords["ir"] = "in_route"
+ search_keywords["hostname"] = "hostname"
+ search_keywords["service"] = "service"
+ search_keywords["s"] = "service"
+ search_keywords["os"] = "os"
+ 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"
+ self.search_parser = SearchParser(self, search_keywords)
+
+ # FIXME: This method doesn't do anything. We just need to support
+ # the type of interface that SearchParser expects in order to use it.
+ # Perhaps, we will eventually refactor the SearchParser a little bit
+ # more?
+ def init_search_dirs(self, junk):
+ pass
+
+ def get_hosts(self):
+ if len(self.search_dict) > 0:
+ return self.filtered_hosts
+ else:
+ return NetworkInventory.get_hosts(self)
+
+ def get_hosts_up(self):
+ if len(self.search_dict) > 0:
+ return [h for h in self.filtered_hosts if h.get_state() == 'up']
+ else:
+ return NetworkInventory.get_hosts_up(self)
+
+ def get_hosts_down(self):
+ if len(self.search_dict) > 0:
+ return [h for h in self.filtered_hosts if h.get_state() == 'down']
+ else:
+ return NetworkInventory.get_hosts_down(self)
+
+ def get_total_host_count(self):
+ return len(self.hosts)
+
+ def _match_all_args(self, host, operator, args):
+ """A helper function that calls the matching function for the given
+ operator and each of its arguments."""
+ for arg in args:
+ positive = True
+ if arg != "" and arg[0] == "!":
+ arg = arg[1:]
+ positive = False
+ if positive != self.__getattribute__(
+ "match_%s" % operator)(host, arg):
+ # No match for this operator
+ return False
+ else:
+ # if the operator is not supported, pretend its true
+ # All arguments for this operator produced a match
+ return True
+
+ def get_host_count(self):
+ return len(self.network_inventory.hosts)
+
+ def match_keyword(self, host, keyword):
+ return (self.match_os(host, keyword) or
+ self.match_target(host, keyword) or
+ self.match_service(host, keyword))
+
+ def match_target(self, host, name):
+ return HostSearch.match_target(host, name)
+
+ def match_in_route(self, host, hop):
+ hops = host.get_trace().get('hops', [])
+ return hop in hops
+
+ def match_hostname(self, host, hostname):
+ return HostSearch.match_hostname(host, hostname)
+
+ def match_service(self, host, service):
+ return HostSearch.match_service(host, service)
+
+ def match_os(self, host, os):
+ return HostSearch.match_os(host, os)
+
+ def match_open(self, host, portno):
+ host_ports = host.get_ports()
+ return HostSearch.match_port(host_ports, portno, "open")
+
+ def match_closed(self, host, portno):
+ host_ports = host.get_ports()
+ return HostSearch.match_port(host_ports, portno, "closed")
+
+ def match_filtered(self, host, portno):
+ host_ports = host.get_ports()
+ return HostSearch.match_port(host_ports, portno, "filtered")
+
+ def match_unfiltered(self, host, portno):
+ host_ports = host.get_ports()
+ return HostSearch.match_port(host_ports, portno, "unfiltered")
+
+ def match_open_filtered(self, host, portno):
+ host_ports = host.get_ports()
+ return HostSearch.match_port(host_ports, portno, "open|filtered")
+
+ def match_closed_filtered(self, host, portno):
+ host_ports = host.get_ports()
+ return HostSearch.match_port(host_ports, portno, "closed|filtered")
+
+ def apply_filter(self, filter_text):
+ self.filter_text = filter_text.lower()
+ self.search_parser.update(self.filter_text)
+ self.filtered_hosts = []
+ for hostname, host in self.hosts.items():
+ # For each host in this scan
+ # Test each given operator against the current host
+ for operator, args in self.search_dict.items():
+ if not self._match_all_args(host, operator, args):
+ # No match => we discard this scan_result
+ break
+ else:
+ # All operator-matching functions have returned True, so this
+ # host satisfies all conditions
+ self.filtered_hosts.append(host)
+
+
+class NetworkInventoryTest(unittest.TestCase):
+ def test_no_external_modification(self):
+ """Test that HostInfo objects passed into the inventory are not
+ modified during aggregation."""
+ scan_1 = zenmapCore.NmapParser.ParserBasics()
+ host_a = zenmapCore.NmapParser.HostInfo()
+ host_a.hostnames = ["a"]
+ host_a.set_state('up')
+ scan_1.start = "1000000000"
+ scan_1.nmap["hosts"] = [host_a]
+
+ scan_2 = zenmapCore.NmapParser.ParserBasics()
+ host_b = zenmapCore.NmapParser.HostInfo()
+ host_b.hostnames = ["b"]
+ host_b.set_state('up')
+ scan_2.start = "1000000001"
+ scan_2.nmap["hosts"] = [host_b]
+
+ inv = NetworkInventory()
+ inv.add_scan(scan_1)
+ inv.add_scan(scan_2)
+
+ self.assertEqual(host_a.hostnames, ["a"])
+ self.assertEqual(host_b.hostnames, ["b"])
+ self.assertEqual(scan_1.nmap["hosts"], [host_a])
+ self.assertEqual(scan_2.nmap["hosts"], [host_b])
+ self.assertEqual(inv.get_hosts_up()[0].hostnames, ["b"])
+
+ def test_cancel_and_remove_scan(self):
+ """Test that canceling and removing a scan does not blow away the
+ inventory hosts"""
+ added_ips = ['10.0.0.1', '10.0.0.2']
+ removed_ips = ['10.0.0.3']
+ scan_1 = zenmapCore.NmapParser.ParserBasics()
+ host_a = zenmapCore.NmapParser.HostInfo()
+ host_a.hostnames = ["a"]
+ host_a.set_ip({'addr': added_ips[0]})
+ scan_1.start = "1000000000"
+ scan_1.nmap["hosts"] = [host_a]
+
+ scan_2 = zenmapCore.NmapParser.ParserBasics()
+ host_b = zenmapCore.NmapParser.HostInfo()
+ host_b.hostnames = ["b"]
+ host_b.set_ip({'addr': added_ips[1]})
+ scan_2.start = "1000000001"
+ scan_2.nmap["hosts"] = [host_b]
+
+ scan_3 = zenmapCore.NmapParser.ParserBasics()
+ host_c = zenmapCore.NmapParser.HostInfo()
+ host_c.hostnames = ["b"]
+ host_c.set_ip({'addr': removed_ips[0]})
+ scan_3.start = "1000000001"
+ scan_3.nmap["hosts"] = [host_c]
+
+ inv = NetworkInventory()
+ inv.add_scan(scan_1)
+ inv.add_scan(scan_2)
+ try:
+ inv.remove_scan(scan_3)
+ except Exception:
+ pass
+ self.assertEqual(added_ips, list(inv.hosts.keys()))
+ self.assertEqual(host_a.hostnames, ["a"])
+ self.assertEqual(host_b.hostnames, ["b"])
+
+
+class FilteredNetworkInventoryTest(unittest.TestCase):
+ def test_filter(self):
+ """Test that the filter still works after moving code to the """
+ """HostSearch class"""
+ from zenmapCore.NmapParser import NmapParser
+ inv = FilteredNetworkInventory()
+ scan = NmapParser()
+ scan.parse_file("test/xml_test9.xml")
+ filter_text = "open:22 os:linux service:openssh"
+ inv.add_scan(scan)
+ inv.apply_filter(filter_text)
+ assert(len(inv.get_hosts()) == 2)
+
+
+class PortChangeTest(unittest.TestCase):
+ def test_port(self):
+ """Verify that the port status (open/filtered/closed) is displayed
+ correctly when the port status changes in newer scans"""
+ from zenmapCore.NmapParser import NmapParser
+ inv = NetworkInventory()
+ scan1 = NmapParser()
+ scan1.parse_file("test/xml_test13.xml")
+ inv.add_scan(scan1)
+ scan2 = NmapParser()
+ scan2.parse_file("test/xml_test14.xml")
+ inv.add_scan(scan2)
+ assert(len(inv.get_hosts()[0].ports) == 2)
+ scan3 = NmapParser()
+ scan3.parse_file("test/xml_test15.xml")
+ inv.add_scan(scan3)
+ assert(len(inv.get_hosts()[0].ports) == 0)
+
+ # Additional test case for when the two scans have port scan ranges
+ # which do not overlap. Example nmap -F -sU versus
+ # nmap -F scanme.nmap.org
+ inv = NetworkInventory()
+ scan4 = NmapParser()
+ scan4.parse_file("test/xml_test16.xml")
+ inv.add_scan(scan4)
+ assert(len(inv.get_hosts()[0].ports) == 3)
+ scan5 = NmapParser()
+ scan5.parse_file("test/xml_test17.xml")
+ inv.add_scan(scan5)
+ assert(len(inv.get_hosts()[0].ports) == 7)
+
+if __name__ == "__main__":
+ unittest.main()
+ if False:
+
+ scan1 = NmapParser("/home/ndwi/scanz/neobee_1.xml")
+ scan1.parse()
+ scan2 = NmapParser("/home/ndwi/scanz/scanme_nmap_org.usr")
+ scan2.parse()
+
+ inventory1 = NetworkInventory()
+ inventory1.add_scan(scan1)
+ inventory1.add_scan(scan2)
+
+ for host in inventory1.get_hosts():
+ print("%s" % host.ip["addr"], end=' ')
+ #if len(host.hostnames) > 0:
+ # print "[%s]:" % host.hostnames[0]["hostname"]
+ #else:
+ # print ":"
+ #for port in host.ports:
+ # print " %s: %s" % (port["portid"], port["port_state"])
+ #print " OS matches: %s" % host.osmatches
+ #print " Ports used: %s" % host.ports_used
+ #print " Trace: %s" % host.trace
+ #if "hops" in host.trace:
+ # print " (%d)" % len(host.trace["hops"])
+
+ inventory1.remove_scan(scan2)
+ print
+ for host in inventory1.get_hosts():
+ print("%s" % host.ip["addr"], end=' ')
+
+ inventory1.add_scan(scan2)
+ print
+ for host in inventory1.get_hosts():
+ print("%s" % host.ip["addr"], end=' ')
+
+ dir = "/home/ndwi/scanz/top01"
+ inventory1.save_to_dir(dir)
+
+ inventory2 = NetworkInventory()
+ inventory2.open_from_dir(dir)
+
+ print()
+ for host in inventory2.get_hosts():
+ print("%s" % host.ip["addr"], end=' ')
diff --git a/zenmap/zenmapCore/NmapCommand.py b/zenmap/zenmapCore/NmapCommand.py
new file mode 100644
index 0000000..d266c0a
--- /dev/null
+++ b/zenmap/zenmapCore/NmapCommand.py
@@ -0,0 +1,269 @@
+#!/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 file contains the definitions of the NmapCommand class, which represents
+# and runs an Nmap command line.
+
+import codecs
+import errno
+import locale
+import sys
+import os
+import tempfile
+import unittest
+
+import zenmapCore.I18N # lgtm[py/unused-import]
+
+import subprocess
+
+import zenmapCore.Paths
+from zenmapCore.NmapOptions import NmapOptions
+from zenmapCore.UmitLogging import log
+from zenmapCore.UmitConf import PathsConfig
+from zenmapCore.Name import APP_NAME
+
+# The [paths] configuration from zenmap.conf, used to get nmap_command_path.
+paths_config = PathsConfig()
+
+log.debug(">>> Platform: %s" % sys.platform)
+
+
+def escape_nmap_filename(filename):
+ """Escape '%' characters so they are not interpreted as strftime format
+ specifiers, which are not supported by Zenmap."""
+ return filename.replace("%", "%%")
+
+
+class NmapCommand(object):
+ """This class represents an Nmap command line. It is responsible for
+ starting, stopping, and returning the results from a command-line scan. A
+ command line is represented as a string but it is split into a list of
+ arguments for execution.
+
+ The normal output (stdout and stderr) are written to the file object
+ self.stdout_file."""
+
+ def __init__(self, command):
+ """Initialize an Nmap command. This creates temporary files for
+ redirecting the various types of output and sets the backing
+ command-line string."""
+ self.command = command
+ self.command_process = None
+
+ self.stdout_file = None
+
+ self.ops = NmapOptions()
+ self.ops.parse_string(command)
+ # Replace the executable name with the value of nmap_command_path.
+ self.ops.executable = paths_config.nmap_command_path
+
+ # Normally we generate a random temporary filename to save XML output
+ # to. If we find -oX or -oA, the user has chosen his own output file.
+ # Set self.xml_is_temp to False and don't delete the file when we're
+ # done.
+ self.xml_is_temp = True
+ self.xml_output_filename = None
+ if self.ops["-oX"]:
+ self.xml_is_temp = False
+ self.xml_output_filename = self.ops["-oX"]
+ if self.ops["-oA"]:
+ self.xml_is_temp = False
+ self.xml_output_filename = self.ops["-oA"] + ".xml"
+
+ # Escape '%' to avoid strftime expansion.
+ for op in ("-oA", "-oX", "-oG", "-oN", "-oS"):
+ if self.ops[op]:
+ self.ops[op] = escape_nmap_filename(self.ops[op])
+
+ if self.xml_is_temp:
+ fh, self.xml_output_filename = tempfile.mkstemp(
+ prefix=APP_NAME + "-", suffix=".xml")
+ os.close(fh)
+ self.ops["-oX"] = escape_nmap_filename(self.xml_output_filename)
+
+ log.debug(">>> Temporary files:")
+ log.debug(">>> XML OUTPUT: %s" % self.xml_output_filename)
+
+ def close(self):
+ """Close and remove temporary output files used by the command."""
+ self.stdout_file.close()
+ if self.xml_is_temp:
+ try:
+ os.remove(self.xml_output_filename)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+
+ def kill(self):
+ """Kill the nmap subprocess."""
+ from time import sleep
+
+ log.debug(">>> Killing scan process %s" % self.command_process.pid)
+
+ if sys.platform != "win32":
+ try:
+ from signal import SIGTERM, SIGKILL
+ os.kill(self.command_process.pid, SIGTERM)
+ for i in range(10):
+ sleep(0.5)
+ if self.command_process.poll() is not None:
+ # Process has been TERMinated
+ break
+ else:
+ log.debug(">>> SIGTERM has not worked even after waiting for 5 seconds. Using SIGKILL.") # noqa
+ os.kill(self.command_process.pid, SIGKILL)
+ self.command_process.wait()
+ except Exception:
+ pass
+ else:
+ try:
+ import ctypes
+ ctypes.windll.kernel32.TerminateProcess(
+ int(self.command_process._handle), -1)
+ except Exception:
+ pass
+
+ def get_path(self):
+ """Return a value for the PATH environment variable that is appropriate
+ for the current platform. It will be the PATH from the environment plus
+ possibly some platform-specific directories."""
+ path_env = os.getenv("PATH")
+ if path_env is None:
+ search_paths = []
+ else:
+ search_paths = path_env.split(os.pathsep)
+ for path in zenmapCore.Paths.get_extra_executable_search_paths():
+ if path not in search_paths:
+ search_paths.append(path)
+ return os.pathsep.join(search_paths)
+
+ def run_scan(self, stderr=None):
+ """Run the command represented by this class."""
+
+ # We don't need a file name for stdout output, just a handle. A
+ # TemporaryFile is deleted as soon as it is closed, and in Unix is
+ # unlinked immediately after creation so it's not even visible.
+ f = tempfile.TemporaryFile(mode="r", prefix=APP_NAME + "-stdout-")
+ self.stdout_file = f
+ if stderr is None:
+ stderr = f
+
+ search_paths = self.get_path()
+ env = dict(os.environ)
+ env["PATH"] = search_paths
+ log.debug("PATH=%s" % env["PATH"])
+
+ command_list = self.ops.render()
+ log.debug("Running command: %s" % repr(command_list))
+
+ startupinfo = None
+ if sys.platform == "win32":
+ # This keeps a terminal window from opening.
+ startupinfo = subprocess.STARTUPINFO()
+ try:
+ startupinfo.dwFlags |= \
+ subprocess._subprocess.STARTF_USESHOWWINDOW
+ except AttributeError:
+ # This name is used before Python 2.6.5.
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+
+ self.command_process = subprocess.Popen(command_list, bufsize=1,
+ universal_newlines=True,
+ stdin=subprocess.PIPE,
+ stdout=f,
+ stderr=stderr,
+ startupinfo=startupinfo,
+ env=env)
+
+ def scan_state(self):
+ """Return the current state of a running scan. A return value of True
+ means the scan is running and a return value of False means the scan
+ subprocess completed successfully. If the subprocess terminated with an
+ error an exception is raised. The scan must have been started with
+ run_scan before calling this method."""
+ if self.command_process is None:
+ raise Exception("Scan is not running yet!")
+
+ state = self.command_process.poll()
+
+ if state is None:
+ return True # True means that the process is still running
+ elif state == 0:
+ return False # False means that the process had a successful exit
+ else:
+ log.warning("An error occurred during the scan execution!")
+ log.warning("Command that raised the exception: '%s'" %
+ self.ops.render_string())
+ log.warning("Scan output:\n%s" % self.get_output())
+
+ raise Exception(
+ "An error occurred during the scan execution!\n\n'%s'" %
+ self.get_output())
+
+ def get_output(self):
+ """Return the complete contents of the self.stdout_file. This modifies
+ the file pointer."""
+ self.stdout_file.seek(0)
+ return self.stdout_file.read()
+
+ def get_xml_output_filename(self):
+ """Return the name of the XML (-oX) output file."""
+ return self.xml_output_filename
+
+if __name__ == '__main__':
+ unittest.TextTestRunner().run(
+ unittest.TestLoader().loadTestsFromTestCase(SplitQuotedTest))
diff --git a/zenmap/zenmapCore/NmapOptions.py b/zenmap/zenmapCore/NmapOptions.py
new file mode 100644
index 0000000..296c7b0
--- /dev/null
+++ b/zenmap/zenmapCore/NmapOptions.py
@@ -0,0 +1,1406 @@
+#!/usr/bin/env python3
+
+# This is an Nmap command line parser. It has two main parts:
+#
+# getopt_long_only_extras, which is like getopt_long_only with robust
+# handling of unknown options.
+#
+# NmapOptions, a class representing a set of Nmap options.
+#
+# NmapOptions is the class for external use. NmapOptions.parse parses a list of
+# a command followed by command-line arguments. NmapOptions.render returns a
+# list of of a command followed by arguments. NmapOptions.parse_string and
+# NmapOptions.render_string first split strings into lists, following certain
+# quoting rules.
+#
+# >>> ops = NmapOptions()
+# >>> ops.parse(["nmap", "-v", "--script", "safe", "localhost"])
+# >>> ops.executable
+# 'nmap'
+# >>> ops.target_specs
+# ['localhost']
+# >>> ops["-v"]
+# 1
+# >>> ops["--script"]
+# 'safe'
+#
+# The command line may be modified by accessing member variables:
+#
+# >>> ops.executable = "C:\Program Files\Nmap\nmap.exe"
+# >>> ops["-v"] = 2
+# >>> ops["-oX"] = "output.xml"
+# >>> ops.render()
+# ['C:\\Program Files\\Nmap\\nmap.exe', '-v', '-v', '-oX', 'output.xml',
+# '--script', 'safe', 'localhost']
+# >>> ops.render_string()
+# '"C:\\Program Files\\Nmap\\nmap.exe" -v -v -oX output.xml\
+# --script safe localhost'
+#
+# A primary design consideration was robust handling of unknown options. That
+# gives this code a degree of independence from Nmap's own list of options. If
+# an option is added to Nmap but not added here, that option is treated as an
+# "extra," an uninterpreted string that is inserted verbatim into the option
+# list. Because the unknown option may or may not take an argument, pains are
+# taken to avoid interpreting any option ambiguously.
+#
+# Consider the following case, where -x is an unknown option:
+# nmap -x -e eth0 scanme.nmap.org
+# If -x, whatever it is, does not take an argument, it is equivalent to
+# nmap -e eth0 scanme.nmap.org -x
+# that is, a scan of scanme.nmap.org over interface eth0. But if it does take
+# an argument, its argument is "-e", and the command line is the same as
+# nmap eth0 scanme.nmap.org -x -e
+# which is a scan of the two hosts eth0 and scanme.nmap.org, over the default
+# interface. In either case scanme.nmap.org is a target but the other arguments
+# are ambiguous. To resolve this, once an unknown option is found, all
+# following arguments that can be interpreted ambiguously are removed with it
+# and placed in the extras, with normal option processing resumed only when
+# there is no more ambiguity. This ensures that such options maintain their
+# relative order when rendered again to output. In this example "-x -e eth0"
+# will always appear in that order, and the -e option will be uninterpreted.
+#
+# To add a new option, one should do the following:
+# 1) Add a test case to the NmapOptionsTest::test_options() method for the new
+# option and make sure it initially fails.
+# 2) Add the new option to NmapOptions.SHORT_OPTIONS and/or
+# NmapOptions.LONG_OPTIONS.
+# 3) Add an appropriate case to NmapOptions::handle_result(). This should
+# include a line something like
+# self[opt] = True
+# or, if the option has an argument 'arg':
+# self[opt] = arg
+# 4) Add an appropriate case to NmapOptions::render()
+# This should include a check to make sure the option was set in
+# handle_result:
+# if self[opt]:
+# or, if self[opt] contains arguments
+# if self[opt] is not None:
+# If the check passed, then opt should be added to opt_list.
+# 5) Edit profile_editor.xml to display the new option in the GUI.
+# 6) Depending on the option, one may need to edit
+# get_option_check_auxiliary_widget in OptionBuilder.py.
+# 7) Make sure the test case works now.
+
+from functools import reduce
+
+
+class option:
+ """A single option, part of a pool of potential options. It's just a name
+ and a flag saying if the option takes no argument, if an argument is
+ optional, or if an argument is required."""
+ NO_ARGUMENT = 0
+ REQUIRED_ARGUMENT = 1
+ OPTIONAL_ARGUMENT = 2
+
+ def __init__(self, name, has_arg):
+ self.name = name
+ self.has_arg = has_arg
+
+
+def split_quoted(s):
+ """Like str.split, except that no splits occur inside quoted strings, and
+ quoted strings are unquoted."""
+ r = []
+ i = 0
+ while i < len(s) and s[i].isspace():
+ i += 1
+ while i < len(s):
+ part = []
+ while i < len(s) and not s[i].isspace():
+ c = s[i]
+ if c == "\"" or c == "'":
+ begin = c
+ i += 1
+ while i < len(s):
+ c = s[i]
+ if c == begin:
+ i += 1
+ break
+ elif c == "\\":
+ i += 1
+ if i < len(s):
+ c = s[i]
+ # Otherwise, ignore the error and leave the backslash
+ # at the end of the string.
+ part.append(c)
+ i += 1
+ else:
+ part.append(c)
+ i += 1
+ r.append("".join(part))
+ while i < len(s) and s[i].isspace():
+ i += 1
+
+ return r
+
+
+def maybe_quote(s):
+ """Return s quoted if it needs to be, otherwise unchanged."""
+ for c in s:
+ if c == "\"" or c == "\\" or c == "'" or c.isspace():
+ break
+ else:
+ return s
+
+ r = []
+ for c in s:
+ if c == "\"":
+ r.append("\\\"")
+ elif c == "\\":
+ r.append("\\\\")
+ else:
+ r.append(c)
+
+ return "\"" + "".join(r) + "\""
+
+
+def join_quoted(l):
+ return " ".join([maybe_quote(x) for x in l])
+
+
+def make_options(short_opts, long_opts):
+ """Parse a short option specification string and long option tuples into a
+ list of option objects."""
+ options = []
+ for name, has_arg in long_opts:
+ options.append(option(name, has_arg))
+
+ while len(short_opts) > 0:
+ name = short_opts[0]
+ short_opts = short_opts[1:]
+ assert name != ":"
+ num_colons = 0
+ while len(short_opts) > 0 and short_opts[0] == ":":
+ short_opts = short_opts[1:]
+ num_colons += 1
+ if num_colons == 0:
+ has_arg = option.NO_ARGUMENT
+ elif num_colons == 1:
+ has_arg = option.REQUIRED_ARGUMENT
+ else:
+ has_arg = option.OPTIONAL_ARGUMENT
+ options.append(option(name, has_arg))
+
+ return options
+
+lookup_option_cache = {}
+
+
+def lookup_option(name, options):
+ """Find an option with the given (possibly abbreviated) name. None is
+ returned if no options match or if the name is ambiguous (more than one
+ option matches with no exact match)."""
+
+ # This function turns out to be a huge bottleneck. Therefore we memoize it.
+ # We hash on the option name and the id of the options list, because lists
+ # aren't hashable. This means that the options list can't change after the
+ # first time you call this function, or you will get stale results. Turning
+ # the list into a tuple and hashing that is too slow.
+ cache_code = (name, id(options))
+ try:
+ return lookup_option_cache[cache_code]
+ except KeyError:
+ pass
+
+ # Nmap treats '_' the same as '-' in long option names.
+ def canonicalize_name(name):
+ return name.replace("_", "-")
+
+ name = canonicalize_name(name)
+ matches = [o for o in options
+ if canonicalize_name(o.name).startswith(name)]
+ if len(matches) == 0:
+ # No match.
+ lookup_option_cache[cache_code] = None
+ elif len(matches) == 1:
+ # Only one match--not an ambiguous abbreviation.
+ lookup_option_cache[cache_code] = matches[0]
+ else:
+ # More than one match--return only an exact match.
+ for match in matches:
+ if canonicalize_name(match.name) == name:
+ lookup_option_cache[cache_code] = match
+ break
+ else:
+ # No exact matches
+ lookup_option_cache[cache_code] = None
+ return lookup_option_cache[cache_code]
+
+
+def split_option(cmd_arg, options):
+ """Split an option into a name, argument (if any), and possible remainder.
+ It is not an error if the option does not include an argument even though
+ it is required; the caller must take the argument from the next
+ command-line argument. The remainder is what's left over after stripping a
+ single short option that doesn't take an argument. At most one of argument
+ and remainder will be non-None.
+ Examples:
+ >>> split_option("-v", [option("v", option.NO_ARGUMENT)])
+ ('v', None, None)
+ >>> split_option("--min-rate",
+ ... [option("min-rate", option.REQUIRED_ARGUMENT)])
+ ('min-rate', None, None)
+ >>> split_option("--min-rate=100",
+ ... [option("min-rate", option.REQUIRED_ARGUMENT)])
+ ('min-rate', '100', None)
+ >>> split_option("-d9", [option("d", option.OPTIONAL_ARGUMENT)])
+ ('d', '9', None)
+ >>> split_option("-AFn", [option("A", option.NO_ARGUMENT)])
+ ('A', None, '-Fn')
+ >>> split_option("-Amin-rate", [option("A", option.NO_ARGUMENT)])
+ ('A', None, '-min-rate')
+ """
+ if cmd_arg.startswith("--"):
+ name = cmd_arg[2:]
+ index = name.find('=')
+ if index < 0:
+ arg = None
+ else:
+ name, arg = name[:index], name[index + 1:]
+ return name, arg, None
+ elif cmd_arg.startswith("-"):
+ name = cmd_arg[1:]
+ # Check for a lone -.
+ if name == "":
+ return name, None, None
+ # First see if it's really a long option (or a single short option).
+ index = name.find('=')
+ if index < 0:
+ arg = None
+ else:
+ name, arg = name[:index], name[index + 1:]
+ if lookup_option(name, options) is not None:
+ return name, arg, None
+ # No luck. Must be a short option.
+ name = cmd_arg[1]
+ option = lookup_option(name, options)
+ if option is None:
+ # An unknown short option. Return the whole thing.
+ return cmd_arg[1:], None, None
+ rest = cmd_arg[2:]
+ if rest == "":
+ return name, None, None
+ if option.has_arg == option.NO_ARGUMENT:
+ return name, None, "-" + rest
+ else:
+ return name, rest, None
+ else:
+ assert False, cmd_arg
+
+
+def get_option(cmd_args, options):
+ """Find and return the first option (plus a possible option argument) or
+ positional argument from the command-line option list in cmd_args. The
+ return value will have one of the following forms:
+ * a string, representing a positional argument;
+ * an (option, argument) pair (argument may be None);
+ * a (None, extra, ...) tuple, where extra, ... is a chain of an unknown
+ option and its following arguments that cannot be interpreted
+ unambiguously; or
+ * None, at the end of the option list."""
+ if len(cmd_args) == 0:
+ return None
+ cmd_arg = cmd_args.pop(0)
+ if cmd_arg == "--":
+ if len(cmd_args) == 0:
+ return None
+ # Grab the positional argument and replace the --.
+ name = cmd_args[0]
+ cmd_args[0] = "--"
+ return name
+ # A normal positional argument.
+ if not cmd_arg.startswith("-"):
+ return cmd_arg
+ name, arg, remainder = split_option(cmd_arg, options)
+ if remainder is not None:
+ cmd_args.insert(0, remainder)
+ option = lookup_option(name, options)
+ if option is None:
+ # Unrecognized option.
+ if arg is not None:
+ return (None, cmd_arg)
+ else:
+ extras = [None, cmd_arg]
+ # We found an unknown option but we have a problem--we don't know
+ # if it takes an argument or not. So what we do is, we simulate
+ # what would happen both if the option took and argument and if it
+ # didn't. The sync function does that by calling this function in a
+ # loop.
+ rest = sync(cmd_args[1:], cmd_args[:], options)
+ # rest is the part of the argument list that is the same whether or
+ # not the unknown option takes an argument. Put everything up until
+ # rest begins in the extras, then set cmd_args to rest.
+ extras += cmd_args[0:len(cmd_args) - len(rest)]
+ del cmd_args[0:len(cmd_args) - len(rest)]
+ return tuple(extras)
+ elif option.has_arg == option.NO_ARGUMENT and arg is not None:
+ # It has an arg but it shouldn't (like --send-ip=5). Treat it as
+ # an extra.
+ return (None, cmd_arg)
+ elif option.has_arg == option.REQUIRED_ARGUMENT and arg is None:
+ # An argument is required but not yet read.
+ if len(cmd_args) == 0:
+ # No more args. Treat it as an extra.
+ return (None, cmd_arg)
+ else:
+ arg = cmd_args.pop(0)
+ return (option.name, arg)
+ else:
+ return (option.name, arg)
+
+
+def sync(a, b, options):
+ """Given two command-line argument lists, incrementally get an option from
+ whichever is longer until both lists are equal. Return the resulting
+ list."""
+ while a != b:
+ if len(a) > len(b):
+ get_option(a, options)
+ else:
+ get_option(b, options)
+ return a
+
+
+def getopt_long_only_extras(cmd_args, short_opts, long_opts):
+ """This is a generator version of getopt_long_only that additionally has
+ robust handling of unknown options. Each of the items in the sequence it
+ yields will be one of the following:
+ * a string, representing a positional argument;
+ * an (option, argument) pair (argument may be None);
+ * a (None, extra, ...) tuple, where extra, ... is a chain of an unknown
+ option and its following arguments that cannot be interpreted
+ unambiguously; or
+ * None, at the end of the option list."""
+ options = make_options(short_opts, long_opts)
+ # get_option modifies its list of arguments in place. Don't modify the
+ # original list.
+ cmd_args_copy = cmd_args[:]
+ while True:
+ result = get_option(cmd_args_copy, options)
+ if result is None:
+ break
+ yield result
+
+
+class NmapOptions(object):
+ SHORT_OPTIONS = "6Ab:D:d::e:Ffg:hi:M:m:nO::o:P:p:RrS:s:T:v::V"
+ LONG_OPTIONS = (
+ ("allports", option.NO_ARGUMENT),
+ ("append-output", option.NO_ARGUMENT),
+ ("badsum", option.NO_ARGUMENT),
+ ("data-length", option.REQUIRED_ARGUMENT),
+ ("datadir", option.REQUIRED_ARGUMENT),
+ ("debug", option.OPTIONAL_ARGUMENT),
+ ("defeat-rst-ratelimit", option.NO_ARGUMENT),
+ ("dns-servers", option.REQUIRED_ARGUMENT),
+ ("exclude", option.REQUIRED_ARGUMENT),
+ ("excludefile", option.REQUIRED_ARGUMENT),
+ ("fuzzy", option.NO_ARGUMENT),
+ ("help", option.NO_ARGUMENT),
+ ("host-timeout", option.REQUIRED_ARGUMENT),
+ ("iL", option.REQUIRED_ARGUMENT),
+ ("iR", option.REQUIRED_ARGUMENT),
+ ("iflist", option.NO_ARGUMENT),
+ ("initial-rtt-timeout", option.REQUIRED_ARGUMENT),
+ ("ip-options", option.REQUIRED_ARGUMENT),
+ ("log-errors", option.NO_ARGUMENT),
+ ("max-hostgroup", option.REQUIRED_ARGUMENT),
+ ("max-os-tries", option.REQUIRED_ARGUMENT),
+ ("max-parallelism", option.REQUIRED_ARGUMENT),
+ ("max-rate", option.REQUIRED_ARGUMENT),
+ ("max-retries", option.REQUIRED_ARGUMENT),
+ ("max-rtt-timeout", option.REQUIRED_ARGUMENT),
+ ("max-scan-delay", option.REQUIRED_ARGUMENT),
+ ("min-hostgroup", option.REQUIRED_ARGUMENT),
+ ("min-parallelism", option.REQUIRED_ARGUMENT),
+ ("min-rate", option.REQUIRED_ARGUMENT),
+ ("min-retries", option.REQUIRED_ARGUMENT),
+ ("min-rtt-timeout", option.REQUIRED_ARGUMENT),
+ ("mtu", option.REQUIRED_ARGUMENT),
+ ("no-stylesheet", option.NO_ARGUMENT),
+ ("oA", option.REQUIRED_ARGUMENT),
+ ("oG", option.REQUIRED_ARGUMENT),
+ ("oM", option.REQUIRED_ARGUMENT),
+ ("oN", option.REQUIRED_ARGUMENT),
+ ("oS", option.REQUIRED_ARGUMENT),
+ ("oX", option.REQUIRED_ARGUMENT),
+ ("open", option.NO_ARGUMENT),
+ ("osscan-guess", option.NO_ARGUMENT),
+ ("osscan-limit", option.NO_ARGUMENT),
+ ("packet-trace", option.NO_ARGUMENT),
+ ("port-ratio", option.REQUIRED_ARGUMENT),
+ ("privileged", option.NO_ARGUMENT),
+ ("randomize-hosts", option.NO_ARGUMENT),
+ ("reason", option.NO_ARGUMENT),
+ ("release-memory", option.NO_ARGUMENT),
+ ("scan-delay", option.REQUIRED_ARGUMENT),
+ ("scanflags", option.REQUIRED_ARGUMENT),
+ ("sI", option.REQUIRED_ARGUMENT),
+ ("script", option.REQUIRED_ARGUMENT),
+ ("script-args", option.REQUIRED_ARGUMENT),
+ ("script-trace", option.NO_ARGUMENT),
+ ("script-updatedb", option.NO_ARGUMENT),
+ ("script-help", option.REQUIRED_ARGUMENT),
+ ("send-eth", option.NO_ARGUMENT),
+ ("send-ip", option.NO_ARGUMENT),
+ ("servicedb", option.REQUIRED_ARGUMENT),
+ ("source-port", option.REQUIRED_ARGUMENT),
+ ("spoof-mac", option.REQUIRED_ARGUMENT),
+ ("stylesheet", option.REQUIRED_ARGUMENT),
+ ("system-dns", option.NO_ARGUMENT),
+ ("timing", option.REQUIRED_ARGUMENT),
+ ("top-ports", option.REQUIRED_ARGUMENT),
+ ("traceroute", option.NO_ARGUMENT),
+ ("ttl", option.REQUIRED_ARGUMENT),
+ ("unprivileged", option.NO_ARGUMENT),
+ ("verbose", option.OPTIONAL_ARGUMENT),
+ ("version", option.NO_ARGUMENT),
+ ("version-all", option.NO_ARGUMENT),
+ ("version-intensity", option.REQUIRED_ARGUMENT),
+ ("version-light", option.NO_ARGUMENT),
+ ("version-trace", option.NO_ARGUMENT),
+ ("versiondb", option.REQUIRED_ARGUMENT),
+ ("webxml", option.NO_ARGUMENT),
+ )
+
+ # Sets of options that should be treated as equivalent from the point of
+ # view of the external interface. For example, ops["--timing"] means the
+ # same thing as ops["-T"].
+ EQUIVALENT_OPTIONS = (
+ ("debug", "d"),
+ ("help", "h"),
+ ("iL", "i"),
+ ("max-parallelism", "M"),
+ ("osscan-guess", "fuzzy"),
+ ("oG", "oM", "m"),
+ ("oN", "o"),
+ ("sP", "sn"),
+ ("P", "PE", "PI"),
+ ("PA", "PT"),
+ ("P0", "PD", "PN", "Pn"),
+ ("rH", "randomize-hosts"),
+ ("source-port", "g"),
+ ("timing", "T"),
+ ("verbose", "v"),
+ ("version", "V"),
+ )
+ EQUIVALENCE_MAP = {}
+ for set in EQUIVALENT_OPTIONS:
+ base = set[0]
+ aliases = set[1:]
+ for alias in aliases:
+ EQUIVALENCE_MAP[alias] = base
+
+ TIMING_PROFILE_NAMES = {
+ "paranoid": 0, "sneaky": 1, "polite": 2,
+ "normal": 3, "aggressive": 4, "insane": 5
+ }
+
+ def __init__(self):
+ self.options = make_options(self.SHORT_OPTIONS, self.LONG_OPTIONS)
+
+ self.clear()
+
+ def clear(self):
+ self._executable = None
+ self.target_specs = []
+ self.extras = []
+
+ # This is the internal mapping of option names to values.
+ self.d = {}
+
+ def _set_executable(self, executable):
+ self._executable = executable
+
+ executable = property(lambda self: self._executable or "nmap",
+ _set_executable)
+
+ def canonicalize_name(self, name):
+ opt, arg, remainder = split_option(name, self.options)
+ assert remainder is None
+ if arg is None:
+ option = lookup_option(opt, self.options)
+ if option:
+ option = option.name
+ else:
+ option = opt
+ else:
+ option = name.lstrip("-")
+ option = NmapOptions.EQUIVALENCE_MAP.get(option, option)
+ return option
+
+ def __getitem__(self, key):
+ return self.d.get(self.canonicalize_name(key))
+
+ def __setitem__(self, key, value):
+ self.d[self.canonicalize_name(key)] = value
+
+ def setdefault(self, key, default):
+ return self.d.setdefault(self.canonicalize_name(key), default)
+
+ def handle_result(self, result):
+ if isinstance(result, str):
+ # A positional argument.
+ self.target_specs.append(result)
+ return
+ elif result[0] is None:
+ # An unknown option.
+ self.extras.extend(result[1:])
+ return
+
+ # A normal option.
+ opt, arg = result
+ if opt in ("6", "A", "F", "h", "n", "R", "r", "V"):
+ self["-" + opt] = True
+ elif opt in (
+ "allports",
+ "append-output",
+ "badsum",
+ "defeat-rst-ratelimit",
+ "fuzzy",
+ "help",
+ "iflist",
+ "log-errors",
+ "no-stylesheet",
+ "open",
+ "osscan-guess",
+ "osscan-limit",
+ "packet-trace",
+ "privileged",
+ "randomize-hosts",
+ "reason",
+ "release-memory",
+ "script-trace",
+ "script-updatedb",
+ "send-eth",
+ "send-ip",
+ "system-dns",
+ "traceroute",
+ "unprivileged",
+ "version",
+ "version-all",
+ "version-light",
+ "version-trace",
+ "webxml",
+ ):
+ self["--" + opt] = True
+ elif opt in ("b", "D", "e", "g", "i", "iL", "m", "M", "o", "oA", "oG",
+ "oM", "oN", "oS", "oX", "p", "S", "sI"):
+ assert arg is not None
+ if self["-" + opt] is None:
+ self["-" + opt] = arg
+ else:
+ self.extras.extend(("-" + opt, arg))
+ elif opt in (
+ "datadir",
+ "data-length",
+ "dns-servers",
+ "exclude",
+ "excludefile",
+ "host-timeout",
+ "initial-rtt-timeout",
+ "ip-options",
+ "max-hostgroup",
+ "max-os-tries",
+ "max-parallelism",
+ "max-rate",
+ "max-retries",
+ "max-rtt-timeout",
+ "max-scan-delay",
+ "min-hostgroup",
+ "min-parallelism",
+ "min-rate",
+ "min-retries",
+ "min-rtt-timeout",
+ "mtu",
+ "port-ratio",
+ "scan-delay",
+ "scanflags",
+ "script",
+ "script-args",
+ "script-help",
+ "servicedb",
+ "source-port",
+ "spoof-mac",
+ "stylesheet",
+ "top-ports",
+ "ttl",
+ "versiondb",
+ "version-intensity",
+ ):
+ assert arg is not None
+ if self["--" + opt] is None:
+ self["--" + opt] = arg
+ else:
+ self.extras.extend(("--" + opt, arg))
+ elif opt == "d" or opt == "debug":
+ if arg is None:
+ arg = ""
+ try:
+ self["-d"] = int(arg)
+ except ValueError:
+ if reduce(lambda x, y: x and y,
+ [z == "d" for z in arg], True):
+ self.setdefault("-d", 0)
+ self["-d"] += len(arg) + 1
+ else:
+ self.extras.append("-d%s" % arg)
+ elif opt == "f":
+ self.setdefault("-f", 0)
+ self["-f"] += 1
+ elif opt == "iR":
+ if self["-iR"] is None:
+ try:
+ self["-iR"] = int(arg)
+ except ValueError:
+ self.extras.extend(("-iR", arg))
+ else:
+ self.extras.extend(("-iR", arg))
+ elif opt == "O":
+ if arg is None:
+ if self["-O"] is None:
+ self["-O"] = True
+ else:
+ self.extras.append("-O")
+ else:
+ if self["-O"] is None:
+ self["-O"] = arg
+ else:
+ self.extras.append("-O%s" % arg)
+ elif opt == "P":
+ type, ports = arg[:1], arg[1:]
+ if (type == "0" or type == "D" or type == "N" or
+ type == "n" and ports == ""):
+ self["-Pn"] = True
+ elif (type == "" or type == "I" or type == "E") and ports == "":
+ self["-PE"] = True
+ elif type == "M" and ports == "":
+ self["-PM"] = True
+ elif type == "P" and ports == "":
+ self["-PP"] = True
+ elif type == "R" and ports == "":
+ self["-PR"] = True
+ elif type == "S":
+ self["-PS"] = ports
+ elif type == "T" or type == "A":
+ self["-PA"] = ports
+ elif type == "U":
+ self["-PU"] = ports
+ elif type == "O":
+ self["-PO"] = ports
+ elif type == "B":
+ self["-PB"] = ports
+ elif type == "Y":
+ self["-PY"] = ports
+ else:
+ self.extras.append("-P%s" % arg)
+ elif opt == "s":
+ for type in arg:
+ if type in "ACFLMNOPRSTUVWXYZn":
+ self["-s%s" % type] = True
+ else:
+ self.extras.append("-s%s" % type)
+ elif opt == "T" or opt == "timing":
+ if self["-T"] is None:
+ try:
+ self["-T"] = int(arg)
+ except ValueError:
+ try:
+ self["-T"] = self.TIMING_PROFILE_NAMES[arg.lower()]
+ except KeyError:
+ self.extras.extend(("-T", arg))
+ else:
+ self.extras.extend(("-T", arg))
+ elif opt == "v" or opt == "verbose":
+ if arg is None:
+ arg = ""
+ try:
+ self["-v"] = int(arg)
+ if self["-v"] == 0:
+ self["-v"] = -1
+ except ValueError:
+ if reduce(lambda x, y: x and y,
+ [z == "v" for z in arg], True):
+ self.setdefault("-v", 0)
+ self["-v"] += len(arg) + 1
+ else:
+ self.extras.append("-v%s" % arg)
+ else:
+ assert False, (opt, arg)
+
+ def parse(self, opt_list):
+ self.clear()
+
+ if len(opt_list) > 0:
+ self.executable = opt_list[0]
+
+ for result in getopt_long_only_extras(
+ opt_list[1:], self.SHORT_OPTIONS, self.LONG_OPTIONS):
+ self.handle_result(result)
+
+ def parse_string(self, opt_string):
+ self.parse(split_quoted(opt_string))
+
+ def render(self):
+ opt_list = []
+
+ for opt in ("-sA", "-sC", "-sF", "-sL", "-sM", "-sN", "-sO", "-sn",
+ "-sR", "-sS", "-sT", "-sU", "-sV", "-sW", "-sX", "-sY", "-sZ"):
+ if self[opt]:
+ opt_list.append(opt)
+
+ if self["-sI"] is not None:
+ opt_list.extend(("-sI", self["-sI"]))
+
+ for opt in ("-6",):
+ if self[opt]:
+ opt_list.append(opt)
+
+ if self["-p"] is not None:
+ opt_list.extend(("-p", self["-p"]))
+
+ if self["-T"] is not None:
+ opt_list.append("-T%s" % str(self["-T"]))
+
+ if self["-O"] is not None:
+ if isinstance(self["-O"], str):
+ opt_list.append("-O%s" % self["-O"])
+ elif self["-O"]:
+ opt_list.append("-O")
+
+ if self["-A"]:
+ opt_list.append("-A")
+
+ if self["-d"]:
+ if self["-d"] == 1:
+ opt_list.append("-d")
+ elif self["-d"] > 1:
+ opt_list.append("-d%s" % self["-d"])
+
+ if self["-f"]:
+ opt_list.extend(["-f"] * self["-f"])
+ if self["-v"]:
+ if self["-v"] == -1:
+ opt_list.append("-v0")
+ opt_list.extend(["-v"] * self["-v"])
+
+ if self["-F"]:
+ opt_list.append("-F")
+ if self["-n"]:
+ opt_list.append("-n")
+
+ if self["-iL"] is not None:
+ opt_list.extend(("-iL", self["-iL"]))
+ if self["-iR"] is not None:
+ opt_list.extend(("-iR", str(self["-iR"])))
+
+ for opt in ("-oA", "-oG", "-oN", "-oS", "-oX"):
+ if self[opt] is not None:
+ opt_list.extend((opt, self[opt]))
+
+ for opt in ("--min-hostgroup", "--max-hostgroup",
+ "--min-parallelism", "--max-parallelism",
+ "--min-rtt-timeout", "--max-rtt-timeout",
+ "--initial-rtt-timeout",
+ "--scan-delay", "--max-scan-delay",
+ "--min-rate", "--max-rate",
+ "--max-retries", "--max-os-tries", "--host-timeout"):
+ if self[opt] is not None:
+ opt_list.extend((opt, self[opt]))
+
+ for ping_option in ("-Pn", "-PE", "-PM", "-PP", "-PR"):
+ if self[ping_option]:
+ opt_list.append(ping_option)
+ for ping_option in ("-PS", "-PA", "-PU", "-PO", "-PY"):
+ if self[ping_option] is not None:
+ opt_list.append(ping_option + self[ping_option])
+ if self["-PB"] is not None:
+ if isinstance(self["-PB"], str):
+ opt_list.append("-PB" + self["-PB"])
+ elif self["-PB"]:
+ opt_list.append("-PB")
+
+ for opt in (
+ "--allports",
+ "--append-output",
+ "--badsum",
+ "--defeat-rst-ratelimit",
+ "--fuzzy",
+ "--help",
+ "--iflist",
+ "--log-errors",
+ "--no-stylesheet",
+ "--open",
+ "--osscan-guess",
+ "--osscan-limit",
+ "--packet-trace",
+ "--privileged",
+ "-r",
+ "-R",
+ "--randomize-hosts",
+ "--reason",
+ "--release-memory",
+ "--script-trace",
+ "--script-updatedb",
+ "--send-eth",
+ "--send-ip",
+ "--system-dns",
+ "--traceroute",
+ "--unprivileged",
+ "--version",
+ "--version-all",
+ "--version-light",
+ "--version-trace",
+ "--webxml",
+ ):
+ if self[opt]:
+ opt_list.append(opt)
+
+ for opt in (
+ "-b",
+ "-D",
+ "--datadir",
+ "--data-length",
+ "--dns-servers",
+ "-e",
+ "--exclude",
+ "--excludefile",
+ "-g",
+ "--ip-options",
+ "--mtu",
+ "--port-ratio",
+ "-S",
+ "--scanflags",
+ "--script",
+ "--script-args",
+ "--script-help",
+ "--servicedb",
+ "--spoof-mac",
+ "--stylesheet",
+ "--top-ports",
+ "--ttl",
+ "--versiondb",
+ "--version-intensity",
+ ):
+ if self[opt] is not None:
+ opt_list.extend((opt, self[opt]))
+
+ opt_list.extend(self.target_specs)
+
+ opt_list.extend(self.extras)
+
+ return [self.executable] + opt_list
+
+ def render_string(self):
+ return join_quoted(self.render())
+
+import doctest
+import unittest
+
+
+class NmapOptionsTest(unittest.TestCase):
+ def test_clear(self):
+ """Test that a new object starts without defining any options, that the
+ clear method removes all options, and that parsing the empty string or
+ an empty list removes all options."""
+ TEST = "nmap -T4 -A -v localhost --webxml"
+ ops = NmapOptions()
+ self.assertTrue(len(ops.render()) == 1)
+ ops.parse_string(TEST)
+ self.assertFalse(len(ops.render()) == 1)
+ ops.clear()
+ self.assertTrue(len(ops.render()) == 1)
+ ops.parse_string(TEST)
+ ops.parse_string("")
+ self.assertEqual(ops.render_string(), "nmap")
+ ops.parse_string(TEST)
+ ops.parse([])
+ self.assertEqual(ops.render_string(), "nmap")
+
+ def test_default_executable(self):
+ """Test that there is a default executable member set."""
+ ops = NmapOptions()
+ self.assertNotNull(ops.executable)
+
+ def test_default_executable(self):
+ """Test that you can set the executable."""
+ ops = NmapOptions()
+ ops.executable = "foo"
+ self.assertEqual(ops.executable, "foo")
+ self.assertEqual(ops.render(), ["foo"])
+
+ def test_render(self):
+ """Test that the render method returns a list."""
+ TEST = "nmap -T4 -A -v localhost --webxml"
+ ops = NmapOptions()
+ ops.parse_string(TEST)
+ self.assertTrue(type(ops.render()) == list,
+ "type == %s" % type(ops.render))
+
+ def test_quoted(self):
+ """Test that strings can be quoted."""
+ ops = NmapOptions()
+
+ ops.parse_string('nmap --script ""')
+ self.assertEqual(ops["--script"], "")
+ ops.parse_string("nmap --script ''")
+ self.assertEqual(ops["--script"], "")
+
+ ops.parse_string('nmap --script test one two three')
+ self.assertEqual(ops["--script"], "test")
+ self.assertEqual(ops.target_specs, ["one", "two", "three"])
+ ops.parse_string('nmap --script "test" one two three')
+ self.assertEqual(ops["--script"], "test")
+ self.assertEqual(ops.target_specs, ["one", "two", "three"])
+ ops.parse_string('nmap --script "test one" two three')
+ self.assertEqual(ops["--script"], "test one")
+ self.assertEqual(ops.target_specs, ["two", "three"])
+ ops.parse_string('nmap --script test" one" two three')
+ self.assertEqual(ops["--script"], "test one")
+ self.assertEqual(ops.target_specs, ["two", "three"])
+ ops.parse_string('nmap --script test" one"""" two" three')
+ self.assertEqual(ops["--script"], "test one two")
+ self.assertEqual(ops.target_specs, ["three"])
+
+ ops.parse_string("nmap --script test one two three")
+ self.assertEqual(ops["--script"], "test")
+ self.assertEqual(ops.target_specs, ["one", "two", "three"])
+ ops.parse_string("nmap --script 'test' one two three")
+ self.assertEqual(ops["--script"], "test")
+ self.assertEqual(ops.target_specs, ["one", "two", "three"])
+ ops.parse_string("nmap --script 'test one' two three")
+ self.assertEqual(ops["--script"], "test one")
+ self.assertEqual(ops.target_specs, ["two", "three"])
+ ops.parse_string("nmap --script test' one' two three")
+ self.assertEqual(ops["--script"], "test one")
+ self.assertEqual(ops.target_specs, ["two", "three"])
+ ops.parse_string("nmap --script test' one'''' two' three")
+ self.assertEqual(ops["--script"], "test one two")
+ self.assertEqual(ops.target_specs, ["three"])
+
+ ops.parse_string('nmap --script "ab\\\"cd"')
+ self.assertEqual(ops["--script"], "ab\"cd")
+ ops.parse_string('nmap --script "ab\\\\cd"')
+ self.assertEqual(ops["--script"], "ab\\cd")
+ ops.parse_string('nmap --script "ab\\\'cd"')
+ self.assertEqual(ops["--script"], "ab'cd")
+ ops.parse_string("nmap --script 'ab\\\"cd'")
+ self.assertEqual(ops["--script"], 'ab"cd')
+
+ ops.parse_string('nmap "--script" test')
+ self.assertEqual(ops["--script"], "test")
+ ops.parse_string("nmap '--script' test")
+ self.assertEqual(ops["--script"], "test")
+
+ ops.parse_string('"nmap foo" --script test')
+ self.assertEqual(ops.executable, "nmap foo")
+ ops.parse_string("'nmap foo' --script test")
+ self.assertEqual(ops.executable, "nmap foo")
+
+ def test_render_quoted(self):
+ """Test that strings that need to be quoted are quoted."""
+ ops = NmapOptions()
+ ops.parse_string('"/path/ /nmap" --script "test one two three"')
+ self.assertEqual(ops.executable, "/path/ /nmap")
+ self.assertEqual(ops["--script"], "test one two three")
+ self.assertEqual(ops.target_specs, [])
+ s = ops.render_string()
+ ops.parse_string(s)
+ self.assertEqual(ops.executable, "/path/ /nmap")
+ self.assertEqual(ops["--script"], "test one two three")
+ self.assertEqual(ops.target_specs, [])
+
+ def test_end(self):
+ """Test that -- ends argument processing."""
+ ops = NmapOptions()
+ ops.parse_string("nmap -v -- -v")
+ self.assertTrue(ops["-v"] == 1)
+ self.assertTrue(ops.target_specs == ["-v"])
+
+ def test_roundtrip(self):
+ """Test that parsing and re-rendering a previous rendering gives the
+ same thing as the previous rendering."""
+ TESTS = (
+ "nmap",
+ "nmap -v",
+ "nmap -vv",
+ "nmap -d -v",
+ "nmap -d -d",
+ "nmap -d -v -d",
+ "nmap localhost",
+ "nmap -oX - 192.168.0.1 -PS10",
+ )
+ ops = NmapOptions()
+ for test in TESTS:
+ ops.parse_string(test)
+ opt_string_1 = ops.render_string()
+ ops.parse_string(opt_string_1)
+ opt_string_2 = ops.render_string()
+ self.assertEqual(opt_string_1, opt_string_2)
+
+ def test_underscores(self):
+ """Test that underscores in option names are treated the same as
+ dashes (and are canonicalized to dashes)."""
+ ops = NmapOptions()
+ ops.parse_string("nmap --osscan_guess")
+ self.assertTrue("--osscan-guess" in ops.render_string())
+
+ def test_args(self):
+ """Test potentially tricky argument scenarios."""
+ ops = NmapOptions()
+ ops.parse_string("nmap -d9")
+ self.assertTrue(len(ops.target_specs) == 0)
+ self.assertTrue(ops["-d"] == 9, ops["-d"])
+ ops.parse_string("nmap -d 9")
+ self.assertTrue(ops.target_specs == ["9"])
+ self.assertTrue(ops["-d"] == 1)
+
+ def test_repetition(self):
+ """Test options that can be repeated to increase their effect."""
+ ops = NmapOptions()
+ ops.parse_string("nmap -vv")
+ self.assertTrue(ops["-v"] == 2)
+ ops.parse_string("nmap -v -v")
+ self.assertTrue(ops["-v"] == 2)
+ ops.parse_string("nmap -ff")
+ self.assertTrue(ops["-f"] == 2)
+ ops.parse_string("nmap -f -f")
+ self.assertTrue(ops["-f"] == 2)
+ # Note: unlike -d, -v doesn't take an optional numeric argument.
+ ops.parse_string("nmap -d2 -d")
+ self.assertTrue(ops["-d"] == 3)
+
+ def test_scan_types(self):
+ """Test that multiple scan types given to the -s option are all
+ interpreted correctly."""
+ ops = NmapOptions()
+ ops.parse_string("nmap -s")
+ self.assertTrue(ops.extras == ["-s"])
+ ops.parse_string("nmap -sS")
+ self.assertTrue(ops.extras == [])
+ self.assertTrue(ops["-sS"])
+ self.assertTrue(not ops["-sU"])
+ ops.parse_string("nmap -sSU")
+ self.assertTrue(ops["-sS"])
+ self.assertTrue(ops["-sU"])
+
+ def test_extras(self):
+ """Test that unknown arguments are correctly recorded. A few subtleties
+ are tested:
+ 1. Unknown options are not simply discarded.
+ 2. When an unknown option is found, any following arguments that could
+ have a different meaning depending on whether the unknown option
+ takes an argument are moved with the argument to the extras.
+ 3. Any arguments moved to the extras are not otherwise interpreted.
+ 4. Extra options so copied are copied in blocks, keeping their original
+ ordering with each block."""
+ ops = NmapOptions()
+
+ ops.parse_string("nmap --fee")
+ self.assertTrue(ops.extras == ["--fee"])
+ self.assertTrue(ops.render_string() == "nmap --fee")
+
+ # Note: -x is not a real Nmap option.
+
+ ops.parse_string("nmap -x")
+ self.assertTrue(ops.extras == ["-x"])
+ self.assertTrue(ops.render_string() == "nmap -x")
+
+ ops.parse_string("nmap -v --fie scanme.nmap.org -d")
+ self.assertTrue(ops.extras == ["--fie", "scanme.nmap.org"])
+ self.assertTrue(ops["-v"] == 1)
+ self.assertTrue(ops["-d"] == 1)
+ self.assertTrue(len(ops.target_specs) == 0)
+
+ ops.parse_string("nmap -v --foe=5 scanme.nmap.org -d")
+ self.assertTrue(ops.extras == ["--foe=5"])
+ self.assertTrue(ops.target_specs == ["scanme.nmap.org"])
+
+ ops.parse_string("nmap --fum -oX out.xml -v")
+ self.assertTrue(ops.extras == ["--fum", "-oX", "out.xml"])
+ self.assertTrue(ops["-v"] == 1)
+
+ ops.parse_string("nmap -x -A localhost")
+ self.assertTrue(ops.extras == ["-x", "-A"])
+
+ ops.parse_string("nmap -x --fee -A localhost")
+ self.assertTrue(ops.extras == ["-x", "--fee", "-A"])
+
+ ops.parse_string("nmap -x -x --timing 3 localhost")
+ self.assertTrue(ops.extras == ["-x", "-x", "--timing", "3"])
+ self.assertTrue(ops.target_specs == ["localhost"])
+
+ ops.parse_string("nmap -x -x --timing=3 localhost")
+ self.assertTrue(ops.extras == ["-x", "-x", "--timing=3"])
+ self.assertTrue(ops.target_specs == ["localhost"])
+
+ ops.parse_string("nmap -x -Ad9")
+ self.assertTrue(ops.extras == ["-x", "-Ad9"])
+
+ ops.parse_string("nmap -xrest")
+ self.assertTrue(ops.extras == ["-xrest"])
+
+ # Options that can't be given more than once should end up in extras.
+ ops.parse_string("nmap -p 53 -p 80 -O --mtu 50 --mtu 100 -O2")
+ self.assertTrue(ops["-p"] == "53")
+ self.assertTrue(ops["--mtu"] == "50")
+ self.assertTrue(ops["-O"])
+ self.assertTrue(ops.extras == ["-p", "80", "--mtu", "100", "-O2"])
+
+ def test_quirks(self):
+ """Test the handling of constructions whose interpretation isn't
+ specified in documentation, but should match that of GNU getopt."""
+ ops = NmapOptions()
+ # Long options can be written with one dash.
+ ops.parse_string("nmap -min-rate 100")
+ self.assertTrue(ops["--min-rate"] == "100")
+ ops.parse_string("nmap -min-rate=100")
+ self.assertTrue(ops["--min-rate"] == "100")
+
+ # Short options not taking an argument can be followed by a long
+ # option.
+ ops.parse_string("nmap -nFmin-rate 100")
+ self.assertTrue(ops["-n"])
+ self.assertTrue(ops["-F"])
+ self.assertTrue(ops["--min-rate"] == "100")
+
+ # Short options taking an argument consume the rest of the argument.
+ ops.parse_string("nmap -nFp1-100")
+ self.assertTrue(ops["-n"])
+ self.assertTrue(ops["-F"])
+ self.assertTrue(ops["-p"] == "1-100")
+
+ def test_conversion(self):
+ """Test that failed integer conversions cause the option to wind up in
+ the extras."""
+ ops = NmapOptions()
+ ops.parse_string("nmap -d#")
+ self.assertTrue(ops.extras == ["-d#"])
+ ops.parse_string("nmap -T monkeys")
+ self.assertTrue(ops["-T"] is None)
+ self.assertTrue(ops.extras == ["-T", "monkeys"])
+ ops.parse_string("nmap -iR monkeys")
+ self.assertTrue(ops["-iR"] is None)
+ self.assertTrue(ops.extras == ["-iR", "monkeys"])
+
+ def test_read_unknown(self):
+ """Test that getting the value of non-options returns None."""
+ ops = NmapOptions()
+ self.assertEqual(ops["-x"], None)
+ self.assertEqual(ops["--nonoption"], None)
+
+ def test_canonical_option_names(self):
+ """Test that equivalent option names are properly canonicalized, so
+ that ops["--timing"] and ops["-T"] mean the same thing, for example."""
+ EQUIVS = (
+ ("--debug", "-d"),
+ ("--help", "-h"),
+ ("-iL", "-i"),
+ ("--max-parallelism", "-M"),
+ ("--osscan-guess", "--fuzzy"),
+ ("-oG", "-oM", "-m"),
+ ("-oN", "-o"),
+ ("-sP", "-sn"),
+ ("-P", "-PE", "-PI"),
+ ("-PA", "-PT"),
+ ("-P0", "-PD", "-PN", "-Pn"),
+ ("--source-port", "-g"),
+ ("--timing", "-T"),
+ ("--verbose", "-v"),
+ ("--version", "-V"),
+ ("--min-rate", "-min-rate", "--min_rate", "-min_rate")
+ )
+ ops = NmapOptions()
+ for set in EQUIVS:
+ for opt in set:
+ ops.clear()
+ ops[opt] = "test"
+ for other in set:
+ self.assertTrue(ops[other] == "test",
+ "%s and %s not the same" % (opt, other))
+
+ def test_options(self):
+ """Test that all options that are supposed to be supported are really
+ supported. They must be parsed and not as extras, and must produce
+ output on rendering that can be parsed again."""
+ TESTS = ["-" + opt for opt in "6AFfhnRrVv"]
+ TESTS += ["-b host", "-D 192.168.0.1,ME,RND", "-d", "-d -d", "-d2",
+ "-e eth0", "-f -f", "-g 53", "-i input.txt", "-M 100",
+ "-m output.gnmap", "-O", "-O2", "-o output.nmap", "-p 1-100",
+ "-S 192.168.0.1", "-T0", "-v -v"]
+ TESTS += ["-s" + opt for opt in "ACFLMNnOPRSTUVWXYZ"]
+ TESTS += ["-P" + opt for opt in "IEMP0NnDRBSTAUOY"]
+ TESTS += ["-P" + opt + "100" for opt in "STAUOY"]
+ TESTS += [
+ "--version",
+ "--verbose",
+ "--datadir=dir",
+ "--datadir dir",
+ "--servicedb=db",
+ "--servicedb db",
+ "--versiondb=db",
+ "--versiondb db",
+ "--debug",
+ "--debug=3",
+ "--debug 3",
+ "--help",
+ "--iflist",
+ "--release-memory",
+ "--max-os-tries=10",
+ "--max-os-tries 10",
+ "--max-parallelism=10",
+ "--min-parallelism 10",
+ "--timing=0",
+ "--timing 0",
+ "--max-rtt-timeout=10",
+ "--max-rtt-timeout 10",
+ "--min-rtt-timeout=10",
+ "--min-rtt-timeout 10",
+ "--initial-rtt-timeout=10",
+ "--initial-rtt-timeout 10",
+ "--excludefile=file",
+ "--excludefile file",
+ "--exclude=192.168.0.0",
+ "--exclude 192.168.0.0",
+ "--max-hostgroup=10",
+ "--max-hostgroup 10",
+ "--min-hostgroup=10",
+ "--min-hostgroup 10",
+ "--open",
+ "--scanflags=RST,ACK",
+ "--scanflags RST,ACK",
+ "--defeat-rst-ratelimit",
+ "--host-timeout=10",
+ "--host-timeout 10",
+ "--scan-delay=10",
+ "--scan-delay 10",
+ "--max-scan-delay=10",
+ "--max-scan-delay 10",
+ "--max-retries=10",
+ "--max-retries 10",
+ "--source-port=53",
+ "--source-port 53",
+ "--randomize-hosts",
+ "--osscan-limit",
+ "--osscan-guess",
+ "--fuzzy",
+ "--packet-trace",
+ "--version-trace",
+ "--data-length=10",
+ "--data-length 10",
+ "--send-eth",
+ "--send-ip",
+ "--stylesheet=style.xml",
+ "--stylesheet style.xml",
+ "--no-stylesheet",
+ "--webxml",
+ "--privileged",
+ "--unprivileged",
+ "--mtu=1500",
+ "--mtu 1500",
+ "--append-output",
+ "--spoof-mac=00:00:00:00:00:00",
+ "--spoof-mac 00:00:00:00:00:00",
+ "--badsum",
+ "--ttl=64",
+ "--ttl 64",
+ "--traceroute",
+ "--reason",
+ "--allports",
+ "--version-intensity=5",
+ "--version-intensity 5",
+ "--version-light",
+ "--version-all",
+ "--system-dns",
+ "--log-errors",
+ "--dns-servers=localhost",
+ "--dns-servers localhost",
+ "--port-ratio=0.5",
+ "--port-ratio 0.5",
+ "--top-ports=1000",
+ "--top-ports 1000",
+ "--script=script.nse",
+ "--script script.nse",
+ "--script-trace",
+ "--script-updatedb",
+ "--script-args=none",
+ "--script-args none",
+ "--script-help=script.nse",
+ "--script-help script.nse",
+ "--ip-options=S",
+ "--ip-options S",
+ "--min-rate=10",
+ "--min-rate 10",
+ "--max-rate=10",
+ "--max-rate 10",
+ "-iL=input.txt",
+ "-iL input.txt",
+ "-iR=1000",
+ "-iR 1000",
+ "-oA=out",
+ "-oA out",
+ "-oG=out.gnmap",
+ "-oG out.gnmap",
+ "-oM=out.gnmap",
+ "-oM out.gnmap",
+ "-oN=out.nmap",
+ "-oN out.nmap",
+ "-oS=out.skid",
+ "-oS out.skid",
+ "-oX=out.xml",
+ "-oX out.xml",
+ "-sI=zombie.example.com",
+ "-sI zombie.example.com",
+ ]
+
+ # The following options are present in the Nmap source but are not
+ # tested for because they are deprecated or not documented or whatever.
+ # "-I",
+ # "--noninteractive",
+ # "--thc",
+ # "--nogcc",
+ # "-rH",
+ # "-ff",
+ # "-vv",
+ # "-oH",
+
+ ops = NmapOptions()
+ for test in TESTS:
+ ops.parse_string("nmap " + test)
+ opt_list_1 = ops.render()
+ self.assertTrue(len(opt_list_1) > 1, "%s missing on render" % test)
+ self.assertTrue(len(ops.extras) == 0,
+ "%s caused extras: %s" % (test, repr(ops.extras)))
+ ops.parse(opt_list_1)
+ opt_list_2 = ops.render()
+ self.assertTrue(opt_list_1 == opt_list_2,
+ "Result of parsing and rendering %s not parsable again" % (
+ test))
+ self.assertTrue(len(ops.extras) == 0,
+ "Result of parsing and rendering %s left extras: %s" % (
+ test, ops.extras))
+
+
+class SplitQuotedTest(unittest.TestCase):
+ """A unittest class that tests the split_quoted function."""
+
+ def test_split(self):
+ self.assertEqual(split_quoted(''), [])
+ self.assertEqual(split_quoted('a'), ['a'])
+ self.assertEqual(split_quoted('a b c'), 'a b c'.split())
+
+ def test_quotes(self):
+ self.assertEqual(split_quoted('a "b" c'), ['a', 'b', 'c'])
+ self.assertEqual(split_quoted('a "b c"'), ['a', 'b c'])
+ self.assertEqual(split_quoted('a "b c""d e"'), ['a', 'b cd e'])
+ self.assertEqual(split_quoted('a "b c"z"d e"'), ['a', 'b czd e'])
+
+ def test_backslash(self):
+ self.assertEqual(split_quoted('"\\""'), ['"'])
+ self.assertEqual(split_quoted('\\"\\""'), ['\\"'])
+ self.assertEqual(split_quoted('"\\"\\""'), ['""'])
+
+
+if __name__ == "__main__":
+ doctest.testmod()
+ unittest.main()
diff --git a/zenmap/zenmapCore/NmapParser.py b/zenmap/zenmapCore/NmapParser.py
new file mode 100644
index 0000000..bf9ad05
--- /dev/null
+++ b/zenmap/zenmapCore/NmapParser.py
@@ -0,0 +1,1345 @@
+#!/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 locale
+import time
+import socket
+import copy
+
+from io import StringIO
+
+# Prevent loading PyXML
+import xml
+xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x]
+
+from xml.sax import make_parser
+from xml.sax import SAXException
+from xml.sax.handler import ContentHandler, EntityResolver
+from xml.sax.saxutils import XMLGenerator
+from xml.sax.xmlreader import AttributesImpl as Attributes
+
+import zenmapCore.I18N # lgtm[py/unused-import]
+from zenmapCore.NmapOptions import NmapOptions, join_quoted
+from zenmapCore.StringPool import unique
+
+# The version of the Nmap DTD this file understands and emits.
+XML_OUTPUT_VERSION = "1.04"
+
+
+class HostInfo(object):
+ def __init__(self):
+ self.comment = None
+ self._tcpsequence = {}
+ self._osmatches = []
+ self._ports = []
+ self._ports_used = []
+ self._extraports = []
+ self._uptime = {}
+ self._hostnames = []
+ self._tcptssequence = {}
+ self._ipidsequence = {}
+ self._ip = None
+ self._ipv6 = None
+ self._mac = None
+ self._state = ''
+ self._comment = ''
+ self._trace = {}
+
+ def make_clone(self):
+ clone = HostInfo()
+ clone.comment = self.comment
+ clone._tcpsequence = copy.deepcopy(self._tcpsequence)
+ clone._osmatches = copy.deepcopy(self._osmatches)
+ clone._ports = copy.deepcopy(self._ports)
+ clone._ports_used = self._ports_used
+ clone._extraports = self._extraports
+ clone._uptime = copy.deepcopy(self._uptime)
+ clone._hostnames = copy.deepcopy(self._hostnames)
+ clone._tcptssequence = copy.deepcopy(self._tcptssequence)
+ clone._ipidsequence = copy.deepcopy(self._ipidsequence)
+ clone._ip = copy.deepcopy(self._ip)
+ clone._ipv6 = copy.deepcopy(self._ipv6)
+ clone._mac = copy.deepcopy(self._mac)
+ clone._state = self._state
+ clone._comment = self._comment
+ clone._trace = copy.deepcopy(self._trace)
+
+ return clone
+
+ # tcpsequence is a dict of the form
+ # {'index': u'203',
+ # 'values': u'3637785D,35B440D1,35E9FC3B,3640DB42,355F5931,3601AE14',
+ # 'difficulty': u'Good luck!'}
+ def set_tcpsequence(self, sequence):
+ self._tcpsequence = sequence
+
+ def get_tcpsequence(self):
+ if self._tcpsequence:
+ return self._tcpsequence
+ return {}
+
+ # tcptssequence is a dict of the form
+ # {'values': u'71D0483C,71D048A3,71D0490C,71D04973,71D049DB,71D04A45',
+ # 'class': u'1000HZ'}
+ def set_tcptssequence(self, sequence):
+ self._tcptssequence = sequence
+
+ def get_tcptssequence(self):
+ if self._tcptssequence:
+ return self._tcptssequence
+ return {}
+
+ # ipidsequence is a dict of the form
+ # {'values': u'0,0,0,0,0,0', 'class': u'All zeros'}
+ def set_ipidsequence(self, sequence):
+ self._ipidsequence = sequence
+
+ def get_ipidsequence(self):
+ if self._ipidsequence:
+ return self._ipidsequence
+ return {}
+
+ # osmatches is a list of dicts of the form
+ # {'name': u'Linux 2.6.24', 'accuracy': u'98', 'line': u'1000',
+ # 'osclasses': ...}
+ # where each 'osclasses' element is a dict of the form
+ # {'vendor': u'Linux', 'osfamily': u'Linux', 'type': u'general purpose',
+ # 'osgen': u'2.6.X', 'accuracy': u'98'}
+ def set_osmatches(self, matches):
+ self._osmatches = matches
+
+ def get_osmatches(self):
+ return self._osmatches
+
+ def get_best_osmatch(self):
+ """Return the OS match with the highest accuracy."""
+ if not self._osmatches:
+ return None
+
+ def osmatch_key(osmatch):
+ try:
+ return -float(osmatch["accuracy"])
+ except ValueError:
+ return 0
+
+ return sorted(self._osmatches, key=osmatch_key)[0]
+
+ # ports_used is a list like
+ # [{'state': u'open', 'portid': u'22', 'proto': u'tcp'},
+ # {'state': u'closed', 'portid': u'25', 'proto': u'tcp'},
+ # {'state': u'closed', 'portid': u'44054', 'proto': u'udp'}]
+ # but not all three elements are necessarily present.
+ def set_ports_used(self, ports):
+ self._ports_used = ports
+
+ def get_ports_used(self):
+ return self._ports_used
+
+ # uptime is a dict of the form
+ # {'seconds': u'1909493', 'lastboot': u'Wed Jul 2 06:48:31 2008'}
+ def set_uptime(self, uptime):
+ self._uptime = uptime
+
+ def get_uptime(self):
+ if self._uptime:
+ return self._uptime
+
+ # Avoid empty dict return
+ return {"seconds": "", "lastboot": ""}
+
+ # ports is an array containing dicts of the form
+ # {'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'}
+ def set_ports(self, ports):
+ self._ports = ports
+
+ def get_ports(self):
+ return self._ports
+
+ # extraports is an array of dicts of the form
+ # {'count': u'1709', 'state': u'filtered'}
+ def set_extraports(self, port_list):
+ self._extraports = port_list
+
+ def get_extraports(self):
+ return self._extraports
+
+ # hostnames is a list containing dicts of the form
+ # [{'hostname': u'scanme.nmap.org', 'hostname_type': u'PTR'}]
+ def set_hostnames(self, hostname_list):
+ self._hostnames = hostname_list
+
+ def get_hostnames(self):
+ return self._hostnames
+
+ # ip, ipv6, and mac are either None or dicts of the form
+ # {'vendor': u'', 'type': u'ipv4', 'addr': u'64.13.134.52'}
+ def set_ip(self, addr):
+ self._ip = addr
+
+ def get_ip(self):
+ return self._ip
+
+ def set_mac(self, addr):
+ self._mac = addr
+
+ def get_mac(self):
+ return self._mac
+
+ def set_ipv6(self, addr):
+ self._ipv6 = addr
+
+ def get_ipv6(self):
+ return self._ipv6
+
+ def get_addrs_for_sort(self):
+ """Return a list of addresses as opaque values sorted such that
+ 1) IPv4 comes before IPv6 comes before MAC, and
+ 2) addresses are sorted according to their binary values, not their
+ string representation.
+ Use this function to the the comparison key when sorting a list of
+ hosts by address."""
+ l = []
+ if self.ip:
+ l.append((1, socket.inet_aton(self.ip["addr"])))
+ if self.ipv6:
+ try:
+ l.append((1,
+ socket.inet_pton(socket.AF_INET6, self.ipv6["addr"])))
+ except AttributeError:
+ # Windows doesn't have socket.inet_pton. Go alphabetical.
+ # Encode to a byte string for possible comparison with binary
+ # address strings (which can't be converted to unicode).
+ l.append((1, self.ipv6["addr"].encode("utf-8")))
+ if self.mac:
+ l.append((3, "".join(
+ chr(int(x, 16)) for x in self.mac["addr"].split(":"))))
+ l.sort()
+ return l
+
+ # comment is a string.
+ def get_comment(self):
+ return self._comment
+
+ def set_comment(self, comment):
+ self._comment = comment
+
+ # state is a string like u'up' or u'down'.
+ def set_state(self, status):
+ self._state = status
+
+ def get_state(self):
+ return self._state
+
+ def get_hostname(self):
+ hostname = None
+ if len(self._hostnames) > 0:
+ hostname = self._hostnames[0]["hostname"]
+
+ address = self.ip or self.ipv6 or self.mac
+ if address is not None:
+ address = address["addr"]
+
+ if hostname is not None:
+ if address is not None:
+ return "%s (%s)" % (hostname, address)
+ else:
+ return hostname
+ else:
+ if address is not None:
+ return address
+ else:
+ return _("Unknown Host")
+
+ def get_port_count_by_states(self, states):
+ count = 0
+
+ for p in self.ports:
+ state = p.get('port_state')
+ if state in states:
+ count += 1
+
+ for extra in self.get_extraports():
+ if extra['state'] in states:
+ count += int(extra['count'])
+
+ return count
+
+ def get_open_ports(self):
+ return self.get_port_count_by_states(('open', 'open|filtered'))
+
+ def get_filtered_ports(self):
+ return self.get_port_count_by_states(
+ ('filtered', 'open|filtered', 'closed|filtered'))
+
+ def get_closed_ports(self):
+ return self.get_port_count_by_states(('closed', 'closed|filtered'))
+
+ def get_scanned_ports(self):
+ scanned = 0
+
+ for p in self.ports:
+ scanned += 1
+
+ for extra in self.get_extraports():
+ scanned += int(extra["count"])
+
+ return scanned
+
+ def get_services(self):
+ services = []
+ for p in self.ports:
+ services.append({
+ "service_name": p.get("service_name", _("unknown")),
+ "portid": p.get("portid", ""),
+ "service_version": p.get("service_version",
+ _("Unknown version")),
+ "service_product": p.get("service_product", ""),
+ "service_extrainfo": p.get("service_extrainfo", ""),
+ "port_state": p.get("port_state", _("unknown")),
+ "protocol": p.get("protocol", "")
+ })
+ return services
+
+ def get_trace(self):
+ return self._trace
+
+ def set_trace(self, trace):
+ self._trace = trace
+
+ def append_trace_hop(self, hop):
+ if "hops" in self._trace:
+ self._trace["hops"].append(hop)
+ else:
+ self._trace["hops"] = [hop]
+
+ def set_trace_error(self, errorstr):
+ self._trace["error"] = errorstr
+
+ # Properties
+ tcpsequence = property(get_tcpsequence, set_tcpsequence)
+ osmatches = property(get_osmatches, set_osmatches)
+ ports = property(get_ports, set_ports)
+ ports_used = property(get_ports_used, set_ports_used)
+ extraports = property(get_extraports, set_extraports)
+ uptime = property(get_uptime, set_uptime)
+ hostnames = property(get_hostnames, set_hostnames)
+ tcptssequence = property(get_tcptssequence, set_tcptssequence)
+ ipidsequence = property(get_ipidsequence, set_ipidsequence)
+ ip = property(get_ip, set_ip)
+ ipv6 = property(get_ipv6, set_ipv6)
+ mac = property(get_mac, set_mac)
+ state = property(get_state, set_state)
+ comment = property(get_comment, set_comment)
+ services = property(get_services)
+ trace = property(get_trace, set_trace)
+
+
+class ParserBasics(object):
+ def __init__(self):
+ # This flag informs us whether the XML output file is temporary (True),
+ # or user specified (False). If any of them is user-specified, it
+ # doesn't get stripped out of the command string in set_nmap_command.
+ self.xml_is_temp = True
+
+ self.nmap = {
+ 'nmaprun': {},
+ 'scaninfo': [],
+ 'verbose': '',
+ 'debugging': '',
+ 'hosts': [],
+ 'runstats': {}
+ }
+
+ self.ops = NmapOptions()
+ self._nmap_output = StringIO()
+
+ def set_xml_is_temp(self, xml_is_temp):
+ # This flag is False if a user has specified his own -oX option - in
+ # which case we not should remove the -oX option from the command
+ # string. A value of True means that we're using a temporary file which
+ # should be removed from the command string (see set_nmap_command).
+ self.xml_is_temp = xml_is_temp
+
+ def get_profile_name(self):
+ return self.nmap['nmaprun'].get('profile_name', '')
+
+ def set_profile_name(self, name):
+ self.nmap['nmaprun']['profile_name'] = name
+
+ def get_targets(self):
+ return self.ops.target_specs
+
+ def set_targets(self, targets):
+ self.ops.target_specs = targets
+
+ def get_nmap_output(self):
+ return self._nmap_output.getvalue()
+
+ def set_nmap_output(self, nmap_output):
+ self._nmap_output.close()
+ del self._nmap_output
+ self._nmap_output = StringIO()
+ self._nmap_output.write(nmap_output)
+
+ def del_nmap_output(self):
+ self._nmap_output.close()
+ del self._nmap_output
+
+ def get_debugging_level(self):
+ return self.nmap.get('debugging', '')
+
+ def set_debugging_level(self, level):
+ self.nmap['debugging'] = level
+
+ def get_verbose_level(self):
+ return self.nmap.get('verbose', '')
+
+ def set_verbose_level(self, level):
+ self.nmap['verbose'] = level
+
+ def get_scaninfo(self):
+ return self.nmap.get('scaninfo', '')
+
+ def set_scaninfo(self, info):
+ self.nmap['scaninfo'] = info
+
+ def get_services_scanned(self):
+ if self._services_scanned is None:
+ return self._services_scanned
+
+ services = []
+ for scan in self.nmap.get('scaninfo', []):
+ services.append(scan['services'])
+
+ self._services_scanned = ','.join(services)
+ return self._services_scanned
+
+ def set_services_scanned(self, services_scanned):
+ self._services_scanned = services_scanned
+
+ def get_nmap_command(self):
+ return self.ops.render_string()
+
+ def set_nmap_command(self, command):
+ self.ops.parse_string(command)
+ if self.xml_is_temp:
+ self.ops["-oX"] = None
+ self.nmap['nmaprun']['args'] = self.ops.render_string()
+
+ def get_scan_type(self):
+ types = []
+ for t in self.nmap.get('scaninfo', []):
+ types.append(t['type'])
+ return types
+
+ def get_protocol(self):
+ protocols = []
+ for proto in self.nmap.get('scaninfo', []):
+ protocols.append(proto['protocol'])
+ return protocols
+
+ def get_num_services(self):
+ if self._num_services is None:
+ return self._num_services
+
+ num = 0
+ for n in self.nmap.get('scaninfo', []):
+ num += int(n['numservices'])
+
+ self._num_services = num
+ return self._num_services
+
+ def set_num_services(self, num_services):
+ self._num_services = num_services
+
+ def get_date(self):
+ epoch = int(self.nmap['nmaprun'].get('start', '0'))
+ return time.localtime(epoch)
+
+ def get_start(self):
+ return self.nmap['nmaprun'].get('start', '0')
+
+ def set_start(self, start):
+ self.nmap['nmaprun']['start'] = start
+
+ def set_date(self, date):
+ if type(date) == type(int):
+ self.nmap['nmaprun']['start'] = date
+ else:
+ raise Exception("Wrong date format. Date should be saved \
+in epoch format!")
+
+ def get_open_ports(self):
+ ports = 0
+
+ for h in self.nmap.get('hosts', []):
+ ports += h.get_open_ports()
+
+ return ports
+
+ def get_filtered_ports(self):
+ ports = 0
+
+ for h in self.nmap.get('hosts', []):
+ ports += h.get_filtered_ports()
+
+ return ports
+
+ def get_closed_ports(self):
+ ports = 0
+
+ for h in self.nmap['hosts']:
+ ports += h.get_closed_ports()
+
+ return ports
+
+ def get_formatted_date(self):
+ return time.strftime("%B %d, %Y - %H:%M", self.get_date())
+
+ def get_scanner(self):
+ return self.nmap['nmaprun'].get('scanner', '')
+
+ def set_scanner(self, scanner):
+ self.nmap['nmaprun']['scanner'] = scanner
+
+ def get_scanner_version(self):
+ return self.nmap['nmaprun'].get('version', '')
+
+ def set_scanner_version(self, version):
+ self.nmap['nmaprun']['version'] = version
+
+ # IPv4
+ def get_ipv4(self):
+ hosts = self.nmap.get('hosts')
+ if hosts is None:
+ return []
+ return [host.ip for host in hosts if host.ip is not None]
+
+ # MAC
+ def get_mac(self):
+ hosts = self.nmap.get('hosts')
+ if hosts is None:
+ return []
+ return [host.mac for host in hosts if host.mac is not None]
+
+ # IPv6
+ def get_ipv6(self):
+ hosts = self.nmap.get('hosts')
+ if hosts is None:
+ return []
+ return [host.ipv6 for host in hosts if host.ipv6 is not None]
+
+ def get_hostnames(self):
+ hostnames = []
+ for host in self.nmap.get('hosts', []):
+ hostnames += host.get_hostnames()
+ return hostnames
+
+ def get_hosts(self):
+ return self.nmap.get('hosts', None)
+
+ def get_runstats(self):
+ return self.nmap.get('runstats', None)
+
+ def set_runstats(self, stats):
+ self.nmap['runstats'] = stats
+
+ def get_hosts_down(self):
+ return int(self.nmap['runstats'].get('hosts_down', '0'))
+
+ def set_hosts_down(self, down):
+ self.nmap['runstats']['hosts_down'] = int(down)
+
+ def get_hosts_up(self):
+ return int(self.nmap['runstats'].get('hosts_up', '0'))
+
+ def set_hosts_up(self, up):
+ self.nmap['runstats']['hosts_up'] = int(up)
+
+ def get_hosts_scanned(self):
+ return int(self.nmap['runstats'].get('hosts_scanned', '0'))
+
+ def set_hosts_scanned(self, scanned):
+ self.nmap['runstats']['hosts_scanned'] = int(scanned)
+
+ def get_finish_time(self):
+ return time.localtime(int(self.nmap['runstats'].get('finished_time',
+ '0')))
+
+ def set_finish_time(self, finish):
+ self.nmap['runstats']['finished_time'] = int(finish)
+
+ def get_finish_epoc_time(self):
+ return int(self.nmap['runstats'].get('finished_time', '0'))
+
+ def set_finish_epoc_time(self, time):
+ self.nmap['runstats']['finished_time'] = time
+
+ def get_scan_name(self):
+ """Get a human-readable string representing this scan."""
+ scan_name = self.nmap.get("scan_name")
+ if scan_name:
+ return scan_name
+ if self.profile_name and self.get_targets():
+ return _("%s on %s") % (self.profile_name,
+ join_quoted(self.get_targets()))
+ return self.get_nmap_command()
+
+ def set_scan_name(self, scan_name):
+ self.nmap["scan_name"] = scan_name
+
+ def get_formatted_finish_date(self):
+ return time.strftime("%B %d, %Y - %H:%M", self.get_finish_time())
+
+ def get_port_protocol_dict(self):
+ #Create a dict of port -> protocol for all ports scanned
+ ports = {}
+ for scaninfo in self.scaninfo:
+ services_string = scaninfo['services'].strip()
+ if services_string == "":
+ services_array = []
+ else:
+ services_array = services_string.split(',')
+ for item in services_array:
+ if item.find('-') == -1:
+ if int(item) not in ports:
+ ports[int(item)] = []
+ ports[int(item)].append(scaninfo['protocol'])
+ else:
+ begin, end = item.split('-')
+ for port in range(int(begin), int(end) + 1):
+ if int(port) not in ports:
+ ports[int(port)] = []
+ ports[int(port)].append(scaninfo['protocol'])
+ return ports
+
+ profile_name = property(get_profile_name, set_profile_name)
+ nmap_output = property(get_nmap_output, set_nmap_output, del_nmap_output)
+ debugging_level = property(get_debugging_level, set_debugging_level)
+ verbose_level = property(get_verbose_level, set_verbose_level)
+ scaninfo = property(get_scaninfo, set_scaninfo)
+ services_scanned = property(get_services_scanned, set_services_scanned)
+ nmap_command = property(get_nmap_command, set_nmap_command)
+ scan_type = property(get_scan_type)
+ protocol = property(get_protocol)
+ num_services = property(get_num_services, set_num_services)
+ date = property(get_date, set_date)
+ open_ports = property(get_open_ports)
+ filtered_ports = property(get_filtered_ports)
+ closed_ports = property(get_closed_ports)
+ formatted_date = property(get_formatted_date)
+ scanner = property(get_scanner, set_scanner)
+ scanner_version = property(get_scanner_version, set_scanner_version)
+ ipv4 = property(get_ipv4)
+ mac = property(get_mac)
+ ipv6 = property(get_ipv6)
+ hostnames = property(get_hostnames)
+ hosts = property(get_hosts)
+ runstats = property(get_runstats, set_runstats)
+ hosts_down = property(get_hosts_down, set_hosts_down)
+ hosts_up = property(get_hosts_up, set_hosts_up)
+ hosts_scanned = property(get_hosts_scanned, set_hosts_scanned)
+ finish_time = property(get_finish_time, set_finish_time)
+ finish_epoc_time = property(get_finish_epoc_time, set_finish_epoc_time)
+ formatted_finish_date = property(get_formatted_finish_date)
+ start = property(get_start, set_start)
+ scan_name = property(get_scan_name, set_scan_name)
+
+ _num_services = None
+ _services_scanned = None
+
+
+class NmapParserSAX(ParserBasics, ContentHandler):
+ def __init__(self):
+ ParserBasics.__init__(self)
+ ContentHandler.__init__(self)
+
+ # The text inside an xml-stylesheet processing instruction, like
+ # 'href="file:///usr/share/nmap/nmap.xsl" type="text/xsl"'.
+ self.xml_stylesheet_data = None
+
+ self.in_interactive_output = False
+ self.in_run_stats = False
+ self.in_host = False
+ self.in_hostnames = False
+ self.in_ports = False
+ self.in_port = False
+ self.in_os = False
+ self.in_trace = False
+ self.list_extraports = []
+
+ self.filename = None
+
+ self.unsaved = False
+
+ def set_parser(self, parser):
+ self.parser = parser
+
+ def parse(self, f):
+ """Parse an Nmap XML file from the file-like object f."""
+ self.parser.parse(f)
+
+ def parse_file(self, filename):
+ """Parse an Nmap XML file from the named file."""
+ with open(filename, "r") as f:
+ self.parse(f)
+ self.filename = filename
+
+ def _parse_nmaprun(self, attrs):
+ run_tag = "nmaprun"
+
+ if self.nmap_output == "" and "nmap_output" in attrs:
+ self.nmap_output = attrs["nmap_output"]
+ self.nmap[run_tag]["profile_name"] = attrs.get("profile_name", "")
+ self.nmap[run_tag]["start"] = attrs.get("start", "")
+ self.nmap[run_tag]["args"] = attrs.get("args", "")
+ self.nmap[run_tag]["scanner"] = attrs.get("scanner", "")
+ self.nmap[run_tag]["version"] = attrs.get("version", "")
+ self.nmap[run_tag]["xmloutputversion"] = attrs.get(
+ "xmloutputversion", "")
+
+ self.nmap_command = self.nmap[run_tag]["args"]
+
+ def _parse_output(self, attrs):
+ if attrs.get("type") != "interactive":
+ return
+ if self.in_interactive_output:
+ raise SAXException("Unexpected nested \"output\" element.")
+ self.in_interactive_output = True
+ self.nmap_output = ""
+
+ def _parse_scaninfo(self, attrs):
+ dic = {}
+
+ dic["type"] = unique(attrs.get("type", ""))
+ dic["protocol"] = unique(attrs.get("protocol", ""))
+ dic["numservices"] = attrs.get("numservices", "")
+ dic["services"] = attrs.get("services", "")
+
+ self.nmap["scaninfo"].append(dic)
+
+ def _parse_verbose(self, attrs):
+ self.nmap["verbose"] = attrs.get("level", "")
+
+ def _parse_debugging(self, attrs):
+ self.nmap["debugging"] = attrs.get("level", "")
+
+ def _parse_runstats_finished(self, attrs):
+ self.nmap["runstats"]["finished_time"] = attrs.get("time", "")
+
+ def _parse_runstats_hosts(self, attrs):
+ self.nmap["runstats"]["hosts_up"] = attrs.get("up", "")
+ self.nmap["runstats"]["hosts_down"] = attrs.get("down", "")
+ self.nmap["runstats"]["hosts_scanned"] = attrs.get("total", "")
+
+ def _parse_host(self, attrs):
+ self.host_info = HostInfo()
+ self.host_info.comment = attrs.get("comment", "")
+
+ def _parse_host_status(self, attrs):
+ self.host_info.set_state(unique(attrs.get("state", "")))
+
+ def _parse_host_address(self, attrs):
+ address_attributes = {"type": unique(attrs.get("addrtype", "")),
+ "vendor": attrs.get("vendor", ""),
+ "addr": attrs.get("addr", "")}
+
+ if address_attributes["type"] == "ipv4":
+ self.host_info.set_ip(address_attributes)
+ elif address_attributes["type"] == "ipv6":
+ self.host_info.set_ipv6(address_attributes)
+ elif address_attributes["type"] == "mac":
+ self.host_info.set_mac(address_attributes)
+
+ def _parse_host_hostname(self, attrs):
+ self.list_hostnames.append({"hostname": attrs.get("name", ""),
+ "hostname_type": attrs.get("type", "")})
+
+ def _parse_host_extraports(self, attrs):
+ self.list_extraports.append({"state": unique(attrs.get("state", "")),
+ "count": attrs.get("count", "")})
+
+ def _parse_host_port(self, attrs):
+ self.dic_port = {"protocol": unique(attrs.get("protocol", "")),
+ "portid": unique(attrs.get("portid", ""))}
+
+ def _parse_host_port_state(self, attrs):
+ self.dic_port["port_state"] = unique(attrs.get("state", ""))
+ self.dic_port["reason"] = unique(attrs.get("reason", ""))
+ self.dic_port["reason_ttl"] = unique(attrs.get("reason_ttl", ""))
+
+ def _parse_host_port_service(self, attrs):
+ self.dic_port["service_name"] = attrs.get("name", "")
+ self.dic_port["service_method"] = unique(attrs.get("method", ""))
+ self.dic_port["service_conf"] = attrs.get("conf", "")
+ self.dic_port["service_product"] = attrs.get("product", "")
+ self.dic_port["service_version"] = attrs.get("version", "")
+ self.dic_port["service_extrainfo"] = attrs.get("extrainfo", "")
+
+ def _parse_host_osmatch(self, attrs):
+ osmatch = self._parsing(attrs, [], ['name', 'accuracy', 'line'])
+ osmatch['osclasses'] = []
+ self.list_osmatch.append(osmatch)
+
+ def _parse_host_portused(self, attrs):
+ self.list_portused.append(self._parsing(
+ attrs, ['state', 'proto', 'portid'], []))
+
+ def _parse_host_osclass(self, attrs):
+ self.list_osclass.append(self._parsing(
+ attrs, ['type', 'vendor', 'osfamily', 'osgen'], ['accuracy']))
+
+ def _parsing(self, attrs, unique_names, other_names):
+ # Returns a dict with the attributes of a given tag with the
+ # attributes names as keys and their respective values
+ dic = {}
+ for at in unique_names:
+ dic[at] = unique(attrs.get(at, ""))
+ for at in other_names:
+ dic[at] = attrs.get(at, "")
+ return dic
+
+ def _parse_host_uptime(self, attrs):
+ self.host_info.set_uptime(self._parsing(
+ attrs, [], ["seconds", "lastboot"]))
+
+ def _parse_host_tcpsequence(self, attrs):
+ self.host_info.set_tcpsequence(self._parsing(
+ attrs, ['difficulty'], ['index', 'values']))
+
+ def _parse_host_tcptssequence(self, attrs):
+ self.host_info.set_tcptssequence(self._parsing(
+ attrs, ['class'], ['values']))
+
+ def _parse_host_ipidsequence(self, attrs):
+ self.host_info.set_ipidsequence(self._parsing(
+ attrs, ['class'], ['values']))
+
+ def _parse_host_trace(self, attrs):
+ trace = {}
+ for attr in ["proto", "port"]:
+ trace[attr] = unique(attrs.get(attr, ""))
+ self.host_info.set_trace(trace)
+
+ def _parse_host_trace_hop(self, attrs):
+ hop = self._parsing(attrs, [], ["ttl", "rtt", "ipaddr", "host"])
+ self.host_info.append_trace_hop(hop)
+
+ def _parse_host_trace_error(self, attrs):
+ self.host_info.set_trace_error(unique(attrs.get("errorstr", "")))
+
+ def processingInstruction(self, target, data):
+ if target == "xml-stylesheet":
+ self.xml_stylesheet_data = data
+
+ def startElement(self, name, attrs):
+ if name == "nmaprun":
+ self._parse_nmaprun(attrs)
+ if name == "output":
+ self._parse_output(attrs)
+ elif name == "scaninfo":
+ self._parse_scaninfo(attrs)
+ elif name == "verbose":
+ self._parse_verbose(attrs)
+ elif name == "debugging":
+ self._parse_debugging(attrs)
+ elif name == "runstats":
+ self.in_run_stats = True
+ elif self.in_run_stats and name == "finished":
+ self._parse_runstats_finished(attrs)
+ elif self.in_run_stats and name == "hosts":
+ self._parse_runstats_hosts(attrs)
+ elif name == "host":
+ self.in_host = True
+ self._parse_host(attrs)
+ self.list_ports = []
+ self.list_extraports = []
+ elif self.in_host and name == "status":
+ self._parse_host_status(attrs)
+ elif self.in_host and name == "address":
+ self._parse_host_address(attrs)
+ elif self.in_host and name == "hostnames":
+ self.in_hostnames = True
+ self.list_hostnames = []
+ elif self.in_host and self.in_hostnames and name == "hostname":
+ self._parse_host_hostname(attrs)
+ elif self.in_host and name == "ports":
+ self.in_ports = True
+ elif self.in_host and self.in_ports and name == "extraports":
+ self._parse_host_extraports(attrs)
+ elif self.in_host and self.in_ports and name == "port":
+ self.in_port = True
+ self._parse_host_port(attrs)
+ elif self.in_host and self.in_ports and \
+ self.in_port and name == "state":
+ self._parse_host_port_state(attrs)
+ elif self.in_host and self.in_ports and \
+ self.in_port and name == "service":
+ self._parse_host_port_service(attrs)
+ elif self.in_host and name == "os":
+ self.in_os = True
+ self.list_portused = []
+ self.list_osmatch = []
+ elif self.in_host and self.in_os and name == "osmatch":
+ self._parse_host_osmatch(attrs)
+ elif self.in_host and self.in_os and name == "portused":
+ self._parse_host_portused(attrs)
+ elif self.in_host and self.in_os and name == "osclass":
+ self.list_osclass = []
+ self._parse_host_osclass(attrs)
+ elif self.in_host and name == "uptime":
+ self._parse_host_uptime(attrs)
+ elif self.in_host and name == "tcpsequence":
+ self._parse_host_tcpsequence(attrs)
+ elif self.in_host and name == "tcptssequence":
+ self._parse_host_tcptssequence(attrs)
+ elif self.in_host and name == "ipidsequence":
+ self._parse_host_ipidsequence(attrs)
+ elif self.in_host and name == "trace":
+ self.in_trace = True
+ self._parse_host_trace(attrs)
+ elif self.in_host and self.in_trace and name == "hop":
+ self._parse_host_trace_hop(attrs)
+ elif self.in_host and self.in_trace and name == "error":
+ self._parse_host_trace_error(attrs)
+
+ def endElement(self, name):
+ if name == "output":
+ self.in_interactive_output = False
+ elif name == "runstats":
+ self.in_run_stats = False
+ elif name == "host":
+ self.in_host = False
+ self.host_info.set_extraports(self.list_extraports)
+ self.host_info.set_ports(self.list_ports)
+ self.nmap["hosts"].append(self.host_info)
+ elif self.in_host and name == "hostnames":
+ self.in_hostnames = False
+ self.host_info.set_hostnames(self.list_hostnames)
+ elif self.in_host and name == "ports":
+ self.in_ports = False
+ elif self.in_host and self.in_ports and name == "port":
+ self.in_port = False
+ self.list_ports.append(self.dic_port)
+ del(self.dic_port)
+ elif self.in_host and self.in_os and name == "osmatch":
+ self.list_osmatch[-1]['osclasses'].extend(self.list_osclass)
+ self.list_osclass = []
+ elif self.in_host and self.in_os and name == "os":
+ self.in_os = False
+ self.host_info.set_ports_used(self.list_portused)
+ self.host_info.set_osmatches(self.list_osmatch)
+
+ del(self.list_portused)
+ del(self.list_osmatch)
+ elif self.in_host and self.in_trace and name == "trace":
+ self.in_trace = False
+
+ def characters(self, content):
+ if self.in_interactive_output:
+ self._nmap_output.write(content)
+
+ def write_text(self, f):
+ """Write the Nmap text output of this object to the file-like object
+ f."""
+ if self.nmap_output == "":
+ return
+ f.write(self.nmap_output)
+
+ def write_xml(self, f):
+ """Write the XML representation of this object to the file-like object
+ f."""
+ writer = XMLGenerator(f)
+ writer.startDocument()
+ if self.xml_stylesheet_data is not None:
+ writer.processingInstruction(
+ "xml-stylesheet", self.xml_stylesheet_data)
+ self._write_nmaprun(writer)
+ self._write_scaninfo(writer)
+ self._write_verbose(writer)
+ self._write_debugging(writer)
+ self._write_output(writer)
+ self._write_hosts(writer)
+ self._write_runstats(writer)
+ writer.endElement("nmaprun")
+ writer.endDocument()
+
+ def get_xml(self):
+ """Return a string containing the XML representation of this scan."""
+ buffer = StringIO()
+ self.write_xml(buffer)
+ string = buffer.getvalue()
+ buffer.close()
+ return string
+
+ def write_xml_to_file(self, filename):
+ """Write the XML representation of this scan to the file whose name is
+ given."""
+ fd = open(filename, "w")
+ self.write_xml(fd)
+ fd.close()
+
+ def _write_output(self, writer):
+ if self.nmap_output == "":
+ return
+ writer.startElement("output", Attributes({"type": "interactive"}))
+ writer.characters(self.nmap_output)
+ writer.endElement("output")
+
+ def _write_runstats(self, writer):
+ ##################
+ # Runstats element
+ writer.startElement("runstats", Attributes(dict()))
+
+ ## Finished element
+ writer.startElement("finished",
+ Attributes(dict(time=str(self.finish_epoc_time),
+ timestr=time.ctime(time.mktime(
+ self.get_finish_time())))))
+ writer.endElement("finished")
+
+ ## Hosts element
+ writer.startElement("hosts",
+ Attributes(dict(up=str(self.hosts_up),
+ down=str(self.hosts_down),
+ total=str(self.hosts_scanned))))
+ writer.endElement("hosts")
+
+ writer.endElement("runstats")
+ # End of Runstats element
+ #########################
+
+ def _write_hosts(self, writer):
+ for host in self.hosts:
+ # Start host element
+ writer.startElement("host",
+ Attributes(dict(comment=host.comment)))
+
+ # Status element
+ writer.startElement("status",
+ Attributes(dict(state=host.state)))
+ writer.endElement("status")
+
+ ##################
+ # Address elements
+ ## IPv4
+ if host.ip is not None:
+ writer.startElement("address",
+ Attributes(dict(addr=host.ip.get("addr", ""),
+ vendor=host.ip.get("vendor", ""),
+ addrtype=host.ip.get("type", ""))))
+ writer.endElement("address")
+
+ ## IPv6
+ if host.ipv6 is not None:
+ writer.startElement("address",
+ Attributes(dict(addr=host.ipv6.get("addr", ""),
+ vendor=host.ipv6.get("vendor", ""),
+ addrtype=host.ipv6.get("type", ""))))
+ writer.endElement("address")
+
+ ## MAC
+ if host.mac is not None:
+ writer.startElement("address",
+ Attributes(dict(addr=host.mac.get("addr", ""),
+ vendor=host.mac.get("vendor", ""),
+ addrtype=host.mac.get("type", ""))))
+ writer.endElement("address")
+ # End of Address elements
+ #########################
+
+ ###################
+ # Hostnames element
+ writer.startElement("hostnames", Attributes({}))
+
+ for hname in host.hostnames:
+ writer.startElement("hostname",
+ Attributes(dict(name=hname.get("hostname", ""),
+ type=hname.get("hostname_type", ""))))
+
+ writer.endElement("hostname")
+
+ writer.endElement("hostnames")
+ # End of Hostnames element
+ ##########################
+
+ ###############
+ # Ports element
+ writer.startElement("ports", Attributes({}))
+
+ ## Extraports elements
+ for ext in host.get_extraports():
+ writer.startElement("extraports",
+ Attributes(dict(count=ext.get("count", ""),
+ state=ext.get("state", ""))))
+ writer.endElement("extraports")
+
+ ## Port elements
+ for p in host.ports:
+ writer.startElement("port",
+ Attributes(dict(portid=p.get("portid", ""),
+ protocol=p.get("protocol", ""))))
+
+ ### Port state
+ writer.startElement("state",
+ Attributes(dict(state=p.get("port_state", ""),
+ reason=p.get("reason", ""),
+ reason_ttl=p.get("reason_ttl", ""))))
+ writer.endElement("state")
+
+ ### Port service info
+ d = {}
+ for xml_attr, member in (("conf", "service_conf"),
+ ("method", "service_method"),
+ ("name", "service_name"),
+ ("product", "service_product"),
+ ("version", "service_version"),
+ ("extrainfo", "service_extrainfo")):
+ if p.get(member):
+ d[xml_attr] = p.get(member)
+ writer.startElement("service", Attributes(d))
+ writer.endElement("service")
+
+ writer.endElement("port")
+
+ writer.endElement("ports")
+ # End of Ports element
+ ######################
+
+ ############
+ # OS element
+ writer.startElement("os", Attributes({}))
+
+ ## Ports used elements
+ for pu in host.ports_used:
+ writer.startElement("portused",
+ Attributes(dict(state=pu.get("state", ""),
+ proto=pu.get("proto", ""),
+ portid=pu.get("portid", ""))))
+ writer.endElement("portused")
+
+ ## Osmatch elements
+ for om in host.osmatches:
+ writer.startElement("osmatch",
+ Attributes(dict(name=om.get("name", ""),
+ accuracy=om.get("accuracy", ""),
+ line=om.get("line", ""))))
+ ## Osclass elements
+ for oc in om['osclasses']:
+ writer.startElement("osclass",
+ Attributes(dict(vendor=oc.get("vendor", ""),
+ osfamily=oc.get("osfamily", ""),
+ type=oc.get("type", ""),
+ osgen=oc.get("osgen", ""),
+ accuracy=oc.get("accuracy", ""))))
+ writer.endElement("osclass")
+ writer.endElement("osmatch")
+
+ writer.endElement("os")
+ # End of OS element
+ ###################
+
+ # Uptime element
+ writer.startElement("uptime",
+ Attributes(dict(seconds=host.uptime.get("seconds", ""),
+ lastboot=host.uptime.get("lastboot", ""))))
+ writer.endElement("uptime")
+
+ #####################
+ # Sequences elements
+ ## TCP Sequence element
+ # Cannot use dict() here, because of the 'class' attribute.
+ writer.startElement("tcpsequence",
+ Attributes({"index": host.tcpsequence.get("index", ""),
+ "difficulty": host.tcpsequence.get("difficulty", ""),
+ "values": host.tcpsequence.get("values", "")}))
+ writer.endElement("tcpsequence")
+
+ ## IP ID Sequence element
+ writer.startElement("ipidsequence",
+ Attributes({"class": host.ipidsequence.get("class", ""),
+ "values": host.ipidsequence.get("values", "")}))
+ writer.endElement("ipidsequence")
+
+ ## TCP TS Sequence element
+ writer.startElement("tcptssequence",
+ Attributes({"class": host.tcptssequence.get("class", ""),
+ "values": host.tcptssequence.get("values", "")}))
+ writer.endElement("tcptssequence")
+ # End of sequences elements
+ ###########################
+
+ ## Trace element
+ if len(host.trace) > 0:
+ writer.startElement("trace",
+ Attributes({"proto": host.trace.get("proto", ""),
+ "port": host.trace.get("port", "")}))
+
+ if "hops" in host.trace:
+ for hop in host.trace["hops"]:
+ writer.startElement("hop",
+ Attributes({"ttl": hop["ttl"],
+ "rtt": hop["rtt"],
+ "ipaddr": hop["ipaddr"],
+ "host": hop["host"]}))
+ writer.endElement("hop")
+
+ if "error" in host.trace:
+ writer.startElement("error",
+ Attributes({"errorstr": host.trace["error"]}))
+ writer.endElement("error")
+
+ writer.endElement("trace")
+ # End of trace element
+ ###########################
+
+ # End host element
+ writer.endElement("host")
+
+ def _write_debugging(self, writer):
+ writer.startElement("debugging", Attributes(dict(
+ level=str(self.debugging_level))))
+ writer.endElement("debugging")
+
+ def _write_verbose(self, writer):
+ writer.startElement("verbose", Attributes(dict(
+ level=str(self.verbose_level))))
+ writer.endElement("verbose")
+
+ def _write_scaninfo(self, writer):
+ for scan in self.scaninfo:
+ writer.startElement("scaninfo",
+ Attributes(dict(type=scan.get("type", ""),
+ protocol=scan.get("protocol", ""),
+ numservices=scan.get("numservices", ""),
+ services=scan.get("services", ""))))
+ writer.endElement("scaninfo")
+
+ def _write_nmaprun(self, writer):
+ writer.startElement("nmaprun",
+ Attributes(dict(args=str(self.nmap_command),
+ profile_name=str(self.profile_name),
+ scanner=str(self.scanner),
+ start=str(self.start),
+ startstr=time.ctime(
+ time.mktime(self.get_date())),
+ version=str(self.scanner_version),
+ xmloutputversion=str(XML_OUTPUT_VERSION))))
+
+ def set_unsaved(self):
+ self.unsaved = True
+
+ def is_unsaved(self):
+ return self.unsaved
+
+
+class OverrideEntityResolver(EntityResolver):
+ """This class overrides the default behavior of xml.sax to download
+ remote DTDs, instead returning blank strings"""
+ empty = StringIO()
+
+ def resolveEntity(self, publicId, systemId):
+ return OverrideEntityResolver.empty
+
+
+def nmap_parser_sax():
+ parser = make_parser()
+ nmap_parser = NmapParserSAX()
+
+ parser.setContentHandler(nmap_parser)
+ parser.setEntityResolver(OverrideEntityResolver())
+ nmap_parser.set_parser(parser)
+
+ return nmap_parser
+
+NmapParser = nmap_parser_sax
+
+
+if __name__ == '__main__':
+ import sys
+
+ file_to_parse = sys.argv[1]
+
+ np = NmapParser()
+ np.parse_file(file_to_parse)
+
+ for host in np.hosts:
+ print("%s:" % host.ip["addr"])
+ print(" Comment:", repr(host.comment))
+ print(" TCP sequence:", repr(host.tcpsequence))
+ print(" TCP TS sequence:", repr(host.tcptssequence))
+ print(" IP ID sequence:", repr(host.ipidsequence))
+ print(" Uptime:", repr(host.uptime))
+ print(" OS Match:", repr(host.osmatches))
+ print(" Ports:")
+ for p in host.ports:
+ print("\t%s" % repr(p))
+ print(" Ports used:", repr(host.ports_used))
+ print(" OS Matches:", repr(host.osmatches))
+ print(" Hostnames:", repr(host.hostnames))
+ print(" IP:", repr(host.ip))
+ print(" IPv6:", repr(host.ipv6))
+ print(" MAC:", repr(host.mac))
+ print(" State:", repr(host.state))
+ if "hops" in host.trace:
+ print(" Trace:")
+ for hop in host.trace["hops"]:
+ print(" ", repr(hop))
+ print()
diff --git a/zenmap/zenmapCore/Paths.py b/zenmap/zenmapCore/Paths.py
new file mode 100644
index 0000000..e441171
--- /dev/null
+++ b/zenmap/zenmapCore/Paths.py
@@ -0,0 +1,243 @@
+#!/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 os.path import join, dirname
+
+import errno
+import os
+import os.path
+import sys
+import shutil
+
+from zenmapCore.BasePaths import base_paths
+from zenmapCore.Name import APP_NAME
+
+
+# Find out the prefix under which data files (interface definition XML,
+# pixmaps, etc.) are stored. This can vary depending on whether we are running
+# in an executable package and what type of package it is, which we check using
+# the sys.frozen attribute. See
+# http://mail.python.org/pipermail/pythonmac-sig/2004-November/012121.html.
+def get_prefix():
+ from site import getsitepackages
+ frozen = getattr(sys, "frozen", None)
+ if frozen == "macosx_app" or "Zenmap.app" in sys.executable:
+ # A py2app .app bundle.
+ return os.path.join(dirname(sys.executable), "..", "Resources")
+ elif frozen is not None:
+ # Assume a py2exe executable.
+ return dirname(sys.executable)
+ elif any(__file__.startswith(pdir) for pdir in getsitepackages()):
+ # Installed in site-packages; use configured prefix.
+ return sys.prefix
+ else:
+ # Normal script execution. Look in the current directory to allow
+ # running from the distribution.
+ return os.path.abspath(os.path.dirname(sys.argv[0]))
+
+prefix = get_prefix()
+
+# These lines are overwritten by the installer to hard-code the installed
+# locations.
+CONFIG_DIR = join(prefix, "share", APP_NAME, "config")
+LOCALE_DIR = join(prefix, "share", APP_NAME, "locale")
+MISC_DIR = join(prefix, "share", APP_NAME, "misc")
+PIXMAPS_DIR = join(prefix, "share", "zenmap", "pixmaps")
+DOCS_DIR = join(prefix, "share", APP_NAME, "docs")
+NMAPDATADIR = join(prefix, "..")
+
+
+def get_extra_executable_search_paths():
+ """Return a list of additional executable search paths as a convenience for
+ platforms where the default PATH is inadequate."""
+ if sys.platform == 'darwin':
+ return ["/usr/local/bin"]
+ elif sys.platform == 'win32':
+ return [dirname(sys.executable)]
+ return []
+
+
+#######
+# Paths
+class Paths(object):
+ """Paths
+ """
+ hardcoded = ["config_dir",
+ "locale_dir",
+ "pixmaps_dir",
+ "misc_dir",
+ "docs_dir"]
+
+ config_files_list = ["config_file",
+ "scan_profile",
+ "version"]
+
+ empty_config_files_list = ["target_list",
+ "recent_scans",
+ "db"]
+
+ misc_files_list = ["options",
+ "profile_editor"]
+
+ def __init__(self):
+ self.config_dir = CONFIG_DIR
+ self.locale_dir = LOCALE_DIR
+ self.pixmaps_dir = PIXMAPS_DIR
+ self.misc_dir = MISC_DIR
+ self.docs_dir = DOCS_DIR
+ self.nmap_dir = NMAPDATADIR
+ self._delayed_incomplete = True
+
+ # Delay initializing these paths so that
+ # zenmapCore.I18N.install_gettext can install _() before modules that
+ # need it get imported
+ def _delayed_init(self):
+ if self._delayed_incomplete:
+ from zenmapCore.UmitOptionParser import option_parser
+ self.user_config_dir = option_parser.get_confdir()
+ self.user_config_file = os.path.join(
+ self.user_config_dir, base_paths['user_config_file'])
+ self._delayed_incomplete = False
+
+ def __getattr__(self, name):
+ if name in self.hardcoded:
+ return self.__dict__[name]
+
+ self._delayed_init()
+ if name in self.config_files_list:
+ return return_if_exists(
+ join(self.user_config_dir, base_paths[name]))
+
+ if name in self.empty_config_files_list:
+ return return_if_exists(
+ join(self.user_config_dir, base_paths[name]), True)
+
+ if name in self.misc_files_list:
+ return return_if_exists(join(self.misc_dir, base_paths[name]))
+
+ try:
+ return self.__dict__[name]
+ except Exception:
+ raise NameError(name)
+
+ def __setattr__(self, name, value):
+ self.__dict__[name] = value
+
+
+def create_dir(path):
+ """Create a directory with os.makedirs without raising an error if the
+ directory already exists."""
+ try:
+ os.makedirs(path)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise
+
+
+def create_user_config_dir(user_dir, template_dir):
+ """Create a user configuration directory by creating the directory if
+ necessary, then copying all the files from the given template directory,
+ skipping any that already exist."""
+ from zenmapCore.UmitLogging import log
+ log.debug(">>> Create user dir at %s" % user_dir)
+ create_dir(user_dir)
+
+ for filename in os.listdir(template_dir):
+ template_filename = os.path.join(template_dir, filename)
+ user_filename = os.path.join(user_dir, filename)
+ # Only copy regular files.
+ if not os.path.isfile(template_filename):
+ continue
+ # Don't overwrite existing files.
+ if os.path.exists(user_filename):
+ log.debug(">>> %s already exists." % user_filename)
+ continue
+ shutil.copyfile(template_filename, user_filename)
+ log.debug(">>> Copy %s to %s." % (template_filename, user_filename))
+
+
+def return_if_exists(path, create=False):
+ path = os.path.abspath(path)
+ if os.path.exists(path):
+ return path
+ elif create:
+ f = open(path, "w")
+ f.close()
+ return path
+ raise Exception("File '%s' does not exist or could not be found!" % path)
+
+############
+# Singleton!
+Path = Paths()
+
+if __name__ == '__main__':
+ print(">>> SAVED DIRECTORIES:")
+ print(">>> LOCALE DIR:", Path.locale_dir)
+ print(">>> PIXMAPS DIR:", Path.pixmaps_dir)
+ print(">>> CONFIG DIR:", Path.config_dir)
+ print()
+ print(">>> FILES:")
+ print(">>> USER CONFIG FILE:", Path.user_config_file)
+ print(">>> CONFIG FILE:", Path.user_config_file)
+ print(">>> TARGET_LIST:", Path.target_list)
+ print(">>> PROFILE_EDITOR:", Path.profile_editor)
+ print(">>> SCAN_PROFILE:", Path.scan_profile)
+ print(">>> RECENT_SCANS:", Path.recent_scans)
+ print(">>> OPTIONS:", Path.options)
+ print()
+ print(">>> DB:", Path.db)
+ print(">>> VERSION:", Path.version)
diff --git a/zenmap/zenmapCore/RecentScans.py b/zenmap/zenmapCore/RecentScans.py
new file mode 100644
index 0000000..6f5e556
--- /dev/null
+++ b/zenmap/zenmapCore/RecentScans.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/
+# *
+# ***************************************************************************/
+
+from os import access, R_OK, W_OK
+from os.path import dirname
+from zenmapCore.Paths import Path
+
+
+class RecentScans(object):
+ def __init__(self):
+ self.temp_list = []
+
+ try:
+ self.recent_scans_file = Path.recent_scans
+ except Exception:
+ self.recent_scans_file = False
+
+ if (self.recent_scans_file and
+ (access(self.recent_scans_file, R_OK and W_OK) or
+ access(dirname(self.recent_scans_file), R_OK and W_OK))):
+ self.using_file = True
+
+ # Recovering saved targets
+ recent_file = open(self.recent_scans_file, "r")
+ self.temp_list = [
+ t for t in recent_file.read().split(";")
+ if t != "" and t != "\n"]
+ recent_file.close()
+ else:
+ self.using_file = False
+
+ def save(self):
+ if self.using_file:
+ recent_file = open(self.recent_scans_file, "w")
+ recent_file.write(";".join(self.temp_list))
+ recent_file.close()
+
+ def add_recent_scan(self, recent_scan):
+ if recent_scan in self.temp_list:
+ return
+
+ self.temp_list.append(recent_scan)
+ self.save()
+
+ def clean_list(self):
+ del self.temp_list
+ self.temp_list = []
+ self.save()
+
+ def get_recent_scans_list(self):
+ t = self.temp_list[:]
+ t.reverse()
+ return t
+
+recent_scans = RecentScans()
+
+if __name__ == "__main__":
+ r = RecentScans()
+ print(">>> Getting empty list:", r.get_recent_scans_list())
+ print(">>> Adding recent scan bla:", r.add_recent_scan("bla"))
+ print(">>> Getting recent scan list:", r.get_recent_scans_list())
+ del r
diff --git a/zenmap/zenmapCore/ScriptArgsParser.py b/zenmap/zenmapCore/ScriptArgsParser.py
new file mode 100644
index 0000000..180bb6c
--- /dev/null
+++ b/zenmap/zenmapCore/ScriptArgsParser.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/
+# *
+# ***************************************************************************/
+
+# This module parses the --script-args and stores in the form of key/value
+# pairs. The logic is same as in nse_main.lua, except that values are not
+# returned as tables but as strings.
+
+import re
+# "^%s*([^'\"%s{},=][^{},=]-)%s*[},=]"
+unquoted_re = re.compile(r'\s*([^\'"\s{},=][^{},=]*?)\s*([},=]|$)')
+# "^%s*(['\"])(.-[^\\])%1%s*[},=]"
+quoted_re = re.compile(r'\s*(([\'"])(.*?[^\\])\2)\s*([},=]|$)')
+# "^%s*(['\"])%1%s*[},=]"
+empty_re = re.compile(r'\s*(([\'"])\2)\s*([},=]|$)')
+
+
+def parse_string(s, start):
+ """Parses a single string that is quoted, unquoted or empty. It returns the
+ found string along with the next starting position """
+ for pattern in unquoted_re, quoted_re, empty_re:
+ m = pattern.match(s, start) or quoted_re.match(s, start)
+ if m:
+ return m.group(1), m.end(1)
+ raise ValueError("No string found at %s." % repr(s[start:]))
+
+
+def next_char(s, start):
+ """Returns the next character and position in the string."""
+ while start < len(s) and s[start].isspace():
+ start += 1
+ if start < len(s):
+ return s[start], start
+ else:
+ return None, start
+
+
+def parse_value(s, start):
+ """If the string starting from start is a name-value pair, returns a
+ name-value tuple. Otherwise returns a plain string."""
+ nc, j = next_char(s, start)
+ if nc == "{":
+ j = parse_table(s, j)
+ return s[start:j], j
+ else:
+ tmp, j = parse_string(s, j)
+ nc, j = next_char(s, j)
+ if nc == "=":
+ # Key/value?
+ j += 1
+ begin = j
+ nc, j = next_char(s, j)
+ if nc == "{":
+ j = parse_table(s, j)
+ else:
+ dummy, j = parse_string(s, j)
+ return (tmp, s[begin:j]), j
+ else:
+ return s[start:j], j
+
+
+def parse_table(s, start):
+ """This function is responsible for parsing a table; i.e, a string that
+ starts with '{'. It returns the position where the balancing pair of braces
+ gets closed."""
+ nc, j = next_char(s, start)
+ if not nc or nc != "{":
+ raise ValueError("No '{' found at %s." % repr(s[start:]))
+ j += 1
+ while True:
+ nc, j = next_char(s, j)
+ if nc == "}":
+ # End of table.
+ return j + 1
+ else:
+ # Replace this with a call to parse_value.
+ v, j = parse_value(s, j)
+ nc, j = next_char(s, j)
+ if nc == ",":
+ j += 1
+
+
+def parse_script_args(s):
+ """Main function responsible for parsing the script args and storing the
+ name-value pairs in a list. If an invalid argument is present it stores the
+ value as None."""
+ args = []
+ nc, j = next_char(s, 0)
+ try:
+ while nc is not None:
+ val, j = parse_value(s, j)
+ if type(val) == str:
+ raise ValueError(
+ "Only name-value pairs expected in parse_script_args.")
+ else:
+ args.append(val)
+ nc, j = next_char(s, j)
+ if nc == ",":
+ j += 1
+ nc, j = next_char(s, j)
+ except ValueError:
+ return None
+ return args
+
+
+def parse_script_args_dict(raw_argument):
+ """Wrapper function that copies the name-value pairs from a list into a
+ dictionary."""
+ args_dict = {}
+ args = parse_script_args(raw_argument)
+ if args is None:
+ return None
+ for item in args:
+ if(len(item) == 2): # only key/value pairs are stored
+ args_dict[item[0]] = item[1]
+ return args_dict
+
+if __name__ == '__main__':
+ TESTS = (
+ ('', []),
+ ('a=b,c=d', [('a', 'b'), ('c', 'd')]),
+ ('a="b=c"', [('a', '"b=c"')]),
+ ('a="def\\"ghi"', [('a', '"def\\"ghi"')]),
+ ('a={one,{two,{three}}}', [('a', '{one,{two,{three}}}')]),
+ ('a={"quoted}quoted"}', [('a', '{"quoted}quoted"}')]),
+ ('a="unterminated', None),
+ ('user=foo,pass=",{}=bar",whois={whodb=nofollow+ripe},'
+ 'userdb=C:\\Some\\Path\\To\\File',
+ [('user', 'foo'), ('pass', '",{}=bar"'),
+ ('whois', '{whodb=nofollow+ripe}'),
+ ('userdb', 'C:\\Some\\Path\\To\\File')]),
+ )
+
+ for test, expected in TESTS:
+ args_dict = parse_script_args_dict(test)
+ print(args_dict)
+ args = parse_script_args(test)
+ if args == expected:
+ print("PASS", test)
+ continue
+ print("FAIL", test)
+ if args is None:
+ print("Parsing error")
+ else:
+ print("%d args" % len(args))
+ for a, v in args:
+ print(a, "=", v)
+ if expected is None:
+ print("Expected parsing error")
+ else:
+ print("Expected %d args" % len(expected))
+ for a, v in expected:
+ print(a, "=", v)
diff --git a/zenmap/zenmapCore/ScriptMetadata.py b/zenmap/zenmapCore/ScriptMetadata.py
new file mode 100644
index 0000000..e348448
--- /dev/null
+++ b/zenmap/zenmapCore/ScriptMetadata.py
@@ -0,0 +1,451 @@
+#!/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 has two classes. ScriptDB is responsible for parsing the
+# script.db file and fetching each script's name and categories.
+# ScriptMetadata gets the description, categories, @usage, @output, and
+# arguments from the script itself.
+
+import re
+import os
+import sys
+
+
+class ScriptDBSyntaxError(SyntaxError):
+ """Exception raised when encountering a syntax error in the script.db"""
+ pass
+
+
+class ScriptDB (object):
+ """Class responsible for parsing the script.db file, fetching script
+ names and categories."""
+ LUA_STRING_ESCAPES = {
+ "a": "\a", "b": "\b", "f": "\f", "n": "\n", "r": "\r",
+ "t": "\t", "v": "\v", "\\": "\\", "\"": "\"", "'": "'", "0": "\0"
+ }
+
+ def __init__(self, script_db_path=None):
+ self.unget_buf = ""
+
+ self.lineno = 1
+ self.line = ""
+ with open(script_db_path, "r") as self.f:
+ self.entries_list = self.parse()
+
+ def syntax_error(self, message):
+ e = ScriptDBSyntaxError(message)
+ e.filename = self.f.name
+ e.lineno = self.lineno
+ e.offset = len(self.line)
+ e.text = self.line
+ return e
+
+ def getchar(self):
+ c = None
+ if self.unget_buf:
+ c = self.unget_buf[-1]
+ self.unget_buf = self.unget_buf[:-1]
+ else:
+ c = self.f.read(1)
+ if c == "\n":
+ self.lineno += 1
+ self.line = ""
+ else:
+ self.line += c
+ return c
+
+ def unget(self, data):
+ if data:
+ self.line = self.line[:-len(data)]
+ self.unget_buf += data
+
+ def parse(self):
+ """Parses a script.db entry and returns it as a dictionary. An entry
+ looks like this:
+ Entry { filename = "afp-brute.nse", categories = \
+ { "auth", "intrusive", } }
+ """
+ entries = []
+ while True:
+ entry = self.parse_entry()
+ if not entry:
+ break
+ entries.append(entry)
+ return entries
+
+ def token(self):
+ """Returns a tuple whose first element is a type ("string", "ident", or
+ "delim") and whose second element is the token text."""
+ c = self.getchar()
+ while c.isspace():
+ c = self.getchar()
+ if not c:
+ return None
+ if c.isalpha() or c == "_":
+ ident = []
+ while c.isalpha() or c.isdigit() or c == "_":
+ ident.append(c)
+ c = self.getchar()
+ self.unget(c)
+ return ("ident", "".join(ident))
+ elif c in "'\"":
+ string = []
+ begin_quote = c
+ c = self.getchar()
+ while c != begin_quote:
+ if c == "\\":
+ repl = None
+ c = self.getchar()
+ if not c:
+ raise self.syntax_error("Unexpected EOF")
+ if c.isdigit():
+ d1 = c
+ d2 = self.getchar()
+ d3 = self.getchar()
+ if d1 and d2 and d3:
+ n = int(d1 + d2 + d3)
+ if n > 255:
+ raise self.syntax_error(
+ "Character code >255")
+ repl = chr(n)
+ else:
+ self.unget(d3)
+ self.unget(d2)
+ if not repl:
+ repl = self.LUA_STRING_ESCAPES.get(c)
+ if not repl:
+ raise self.syntax_error("Unhandled string escape")
+ c = repl
+ string.append(c)
+ c = self.getchar()
+ return ("string", "".join(string))
+ elif c in "{},=":
+ return ("delim", c)
+ else:
+ raise self.syntax_error("Unknown token")
+
+ def expect(self, tokens):
+ for token in tokens:
+ t = self.token()
+ if t != token:
+ raise self.syntax_error(
+ "Unexpected token '%s', expected '%s'" % (
+ t[1], token[1]))
+
+ def parse_entry(self):
+ entry = {}
+ token = self.token()
+ if not token:
+ return None
+ self.expect((("delim", "{"), ("ident", "filename"), ("delim", "=")))
+ token = self.token()
+ if not token or token[0] != "string":
+ raise self.syntax_error("Unexpected non-string token or EOF")
+ entry["filename"] = token[1]
+ self.expect((("delim", ","), ("ident", "categories"),
+ ("delim", "="), ("delim", "{")))
+ entry["categories"] = []
+ token = self.token()
+ if token and token[0] == "string":
+ entry["categories"].append(token[1])
+ token = self.token()
+ while token == ("delim", ","):
+ token = self.token()
+ if token and token[0] == "string":
+ entry["categories"].append(token[1])
+ else:
+ break
+ token = self.token()
+ if token != ("delim", "}"):
+ raise self.syntax_error(
+ "Unexpected token '%s', expected '}'" % (token[1]))
+ token = self.token()
+ if token == ("delim", ","):
+ token = self.token()
+ if token != ("delim", "}"):
+ raise self.syntax_error(
+ "Unexpected token '%s', expected '}'" % (token[1]))
+ return entry
+
+ def get_entries_list(self):
+ return self.entries_list
+
+
+def nsedoc_tags_iter(f):
+ in_doc_comment = False
+ tag_name = None
+ tag_text = None
+ for line in f:
+ # New LuaDoc comment?
+ if re.match(r'^\s*---', line):
+ in_doc_comment = True
+ if not in_doc_comment:
+ continue
+ # New LuaDoc tag?
+ m = re.match(r'^\s*--+\s*@(\w+)\s*(.*)', line, re.S)
+ if m:
+ if tag_name:
+ yield tag_name, tag_text
+ tag_name = None
+ tag_text = None
+ tag_name = m.group(1)
+ tag_text = m.group(2)
+ else:
+ # Still in comment?
+ m = re.match(r'^\s*--+\s*(.*)', line)
+ if m:
+ # Add to text if we're in a tag.
+ if tag_name:
+ tag_text += m.group(1) + "\n"
+ else:
+ in_doc_comment = False
+ if tag_name:
+ yield tag_name, tag_text
+ tag_name = None
+ tag_text = None
+
+
+class ScriptMetadata (object):
+ """Class responsible for parsing all the script information."""
+
+ class Entry (object):
+ """An instance of this class is used to store all the information
+ related to a particular script."""
+ def __init__(self, filename):
+ self.filename = filename
+ self.categories = []
+ self.arguments = [] # Arguments including library arguments.
+ self.license = ""
+ self.author = []
+ self.description = ""
+ self.output = ""
+ self.usage = ""
+
+ url = property(lambda self: "https://nmap.org/nsedoc/scripts/"
+ "%s.html" % (os.path.splitext(self.filename)[0]))
+
+ def __init__(self, scripts_dir, nselib_dir):
+ self.scripts_dir = scripts_dir
+ self.nselib_dir = nselib_dir
+ self.library_arguments = {}
+ self.library_requires = {}
+ self.construct_library_arguments()
+
+ def get_metadata(self, filename):
+ entry = self.Entry(filename)
+ try:
+ entry.description = self.get_string_variable(filename, "description")
+ entry.arguments = self.get_arguments(entry.filename)
+ entry.license = self.get_string_variable(filename, "license")
+ entry.author = self.get_list_variable(filename, "author") or [
+ self.get_string_variable(filename, "author")]
+
+ filepath = os.path.join(self.scripts_dir, filename)
+ with open(filepath, "r") as f:
+ for tag_name, tag_text in nsedoc_tags_iter(f):
+ if tag_name == "output" and not entry.output:
+ entry.output = tag_text
+ elif tag_name == "usage" and not entry.usage:
+ entry.usage = tag_text
+ except IOError as e:
+ entry.description = "Error getting metadata: {}".format(e)
+
+ return entry
+
+ @staticmethod
+ def get_file_contents(filename):
+ with open(filename, "r") as f:
+ contents = f.read()
+ return contents
+
+ def get_string_variable(self, filename, varname):
+ contents = ScriptMetadata.get_file_contents(
+ os.path.join(self.scripts_dir, filename))
+ # Short string?
+ m = re.search(
+ re.escape(varname) + r'\s*=\s*(["\'])(.*?[^\\])\1', contents)
+ if m:
+ return m.group(2)
+ # Long string?
+ m = re.search(
+ re.escape(varname) + r'\s*=\s*\[(=*)\[(.*?)\]\1\]', contents, re.S)
+ if m:
+ return m.group(2)
+ return None
+
+ def get_list_variable(self, filename, varname):
+ contents = ScriptMetadata.get_file_contents(
+ os.path.join(self.scripts_dir, filename))
+ m = re.search(
+ re.escape(varname) + r'\s*=\s*\{(.*?)}', contents)
+ if not m:
+ return None
+ strings = m.group(1)
+ out = []
+ for m in re.finditer(r'(["\'])(.*?[^\\])\1\s*,?', strings, re.S):
+ out.append(m.group(2))
+ return out
+
+ @staticmethod
+ def get_requires(filename):
+ with open(filename, "r") as f:
+ requires = ScriptMetadata.get_requires_from_file(f)
+ return requires
+
+ @staticmethod
+ def get_requires_from_file(f):
+ require_expr = re.compile(r'.*\brequire\s*\(?([\'\"])([\w._-]+)\1\)?')
+ requires = []
+ for line in f.readlines():
+ m = require_expr.match(line)
+ if m:
+ requires.append(m.group(2))
+ return requires
+
+ @staticmethod
+ def get_script_args(filename):
+ with open(filename, "r") as f:
+ args = ScriptMetadata.get_script_args_from_file(f)
+ return args
+
+ @staticmethod
+ def get_script_args_from_file(f):
+ """Extracts a list of script arguments from the file given. Results are
+ returned as a list of (argname, description) tuples."""
+ args = []
+ for tag_name, tag_text in nsedoc_tags_iter(f):
+ m = re.match(r'(\S+)\s+(.*?)', tag_text, re.DOTALL)
+ if (tag_name == "arg" or tag_name == "args") and m:
+ args.append((m.group(1), m.group(2)))
+ return args
+
+ def get_arguments(self, filename):
+ """Returns list of arguments including library arguments on
+ passing the file name."""
+ filepath = os.path.join(self.scripts_dir, filename)
+ script_args = self.get_script_args(filepath)
+
+ # Recursively walk through the libraries required by the script (and
+ # the libraries they require, etc.), adding all arguments.
+ library_args = []
+ seen = set()
+ pool = set(self.get_requires(filepath))
+ while pool:
+ require = pool.pop()
+ if require in seen:
+ continue
+ seen.add(require)
+ sub_requires = self.library_requires.get(require)
+ if sub_requires:
+ pool.update(set(sub_requires))
+ require_args = self.library_arguments.get(require)
+ if require_args:
+ library_args += require_args
+
+ return script_args + library_args
+
+ def construct_library_arguments(self):
+ """Constructs a dictionary of library arguments using library
+ names as keys and arguments as values. Each argument is really a
+ (name, description) tuple."""
+ for filename in os.listdir(self.nselib_dir):
+ filepath = os.path.join(self.nselib_dir, filename)
+ if not os.path.isfile(filepath):
+ continue
+
+ base, ext = os.path.splitext(filename)
+ if ext == ".lua" or ext == ".luadoc":
+ libname = base
+ else:
+ libname = filename
+
+ self.library_arguments[libname] = self.get_script_args(filepath)
+ self.library_requires[libname] = self.get_requires(filepath)
+
+
+def get_script_entries(scripts_dir, nselib_dir):
+ """Merge the information obtained so far into one single entry for
+ each script and return it."""
+ metadata = ScriptMetadata(scripts_dir, nselib_dir)
+ try:
+ scriptdb = ScriptDB(os.path.join(scripts_dir, "script.db"))
+ except IOError:
+ return []
+ entries = []
+ for dbentry in scriptdb.get_entries_list():
+ entry = metadata.get_metadata(dbentry["filename"])
+ # Categories is the only thing ScriptMetadata doesn't take care of.
+ entry.categories = dbentry["categories"]
+ entries.append(entry)
+ return entries
+
+if __name__ == '__main__':
+ import sys
+ for entry in get_script_entries(sys.argv[1], sys.argv[2]):
+ print("*" * 75)
+ print("Filename:", entry.filename)
+ print("Categories:", entry.categories)
+ print("License:", entry.license)
+ print("Author:", entry.author)
+ print("URL:", entry.url)
+ print("Description:", entry.description)
+ print("Arguments:", [x[0] for x in entry.arguments])
+ print("Output:")
+ print(entry.output)
+ print("Usage:")
+ print(entry.usage)
+ print("*" * 75)
diff --git a/zenmap/zenmapCore/SearchResult.py b/zenmap/zenmapCore/SearchResult.py
new file mode 100644
index 0000000..5f9f299
--- /dev/null
+++ b/zenmap/zenmapCore/SearchResult.py
@@ -0,0 +1,546 @@
+#!/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 os.path
+import re
+import io
+import unittest
+
+from glob import glob
+
+from zenmapCore.Name import APP_NAME
+from zenmapCore.NmapOptions import NmapOptions
+from zenmapCore.NmapParser import NmapParser
+from zenmapCore.UmitLogging import log
+
+
+class HostSearch(object):
+ @staticmethod
+ def match_target(host, name):
+ name = name.lower()
+ mac = host.get_mac()
+ ip = host.get_ip()
+ ipv6 = host.get_ipv6()
+
+ if mac and 'addr' in mac:
+ if name in mac['addr'].lower():
+ return True
+ if ip and 'addr' in ip:
+ if name in ip['addr'].lower():
+ return True
+ if ipv6 and 'addr' in ipv6:
+ if name in ipv6['addr'].lower():
+ return True
+
+ if HostSearch.match_hostname(host, name):
+ return True
+ return False
+
+ @staticmethod
+ def match_hostname(host, hostname):
+ hostname = hostname.lower()
+ hostnames = host.get_hostnames()
+ for hn in hostnames:
+ if hostname in hn['hostname'].lower():
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def match_service(host, service):
+ for port in host.get_ports():
+ # We concatenate all useful fields and add them to the list
+ if port['port_state'] not in ['open', 'open|filtered']:
+ continue
+ version = " ".join(
+ port.get(x, "") for x in (
+ "service_name",
+ "service_product",
+ "service_version",
+ "service_extrainfo"
+ )
+ )
+
+ if service in version.lower():
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def match_os(host, os):
+ os = os.lower()
+
+ osmatches = host.get_osmatches()
+
+ for osmatch in osmatches:
+ os_str = osmatch['name'].lower()
+ for osclass in osmatch['osclasses']:
+ os_str += " " + osclass['vendor'].lower() + " " +\
+ osclass['osfamily'].lower() + " " +\
+ osclass['type'].lower()
+ if os in os_str:
+ return True
+
+ return False
+
+ @staticmethod
+ def match_port(host_ports, port, port_state):
+ # Check if the port is parsable, if not return False silently
+ if re.match(r"^\d+$", port) is None:
+ return False
+
+ for hp in host_ports:
+ if hp['portid'] == port and hp['port_state'] == port_state:
+ return True
+
+ return False
+
+
+class SearchResult(object):
+ def __init__(self):
+ """This constructor is always called by SearchResult subclasses."""
+ pass
+
+ def search(self, **kargs):
+ """Performs a search on each parsed scan. Since the 'and' operator is
+ implicit, the search fails as soon as one of the tests fails. The
+ kargs argument is a map having operators as keys and argument lists as
+ values."""
+
+ for scan_result in self.get_scan_results():
+ self.parsed_scan = scan_result
+
+ # Test each given operator against the current parsed result
+ for operator, args in kargs.items():
+ if not self._match_all_args(operator, args):
+ # No match => we discard this scan_result
+ break
+ else:
+ # All operator-matching functions have returned True, so this
+ # scan_result satisfies all conditions
+ yield self.parsed_scan
+
+ def _match_all_args(self, operator, args):
+ """A helper function that calls the matching function for the given
+ operator and each of its arguments."""
+ for arg in args:
+ positive = True
+ if arg != "" and arg[0] == "!":
+ arg = arg[1:]
+ positive = False
+ if positive != self.__getattribute__("match_%s" % operator)(arg):
+ # No match for this operator
+ return False
+ else:
+ # All arguments for this operator produced a match
+ return True
+
+ def get_scan_results(self):
+ # To be implemented by classes that are going to inherit this one
+ pass
+
+ def basic_match(self, keyword, property):
+ if keyword == "*" or keyword == "":
+ return True
+
+ return keyword.lower() in str(
+ self.parsed_scan.__getattribute__(property)).lower()
+
+ def match_keyword(self, keyword):
+ log.debug("Match keyword: %s" % keyword)
+
+ return self.basic_match(keyword, "nmap_output") or \
+ self.match_profile(keyword) or \
+ self.match_target(keyword)
+
+ def match_profile(self, profile):
+ log.debug("Match profile: %s" % profile)
+ log.debug("Comparing: %s == %s ??" % (
+ str(self.parsed_scan.profile_name).lower(),
+ "*%s*" % profile.lower()))
+ return (profile == "*" or profile == "" or
+ profile.lower() in str(self.parsed_scan.profile_name).lower())
+
+ def match_option(self, option):
+ log.debug("Match option: %s" % option)
+
+ if option == "*" or option == "":
+ return True
+
+ ops = NmapOptions()
+ ops.parse_string(self.parsed_scan.get_nmap_command())
+
+ if "(" in option and ")" in option:
+ # The syntax allows matching option arguments as
+ # "opt:option_name(value)". Since we've received only the
+ # "option_name(value)" part, we need to parse it.
+ optname = option[:option.find("(")]
+ optval = option[option.find("(") + 1:option.find(")")]
+
+ val = ops["--" + optname]
+ if val is None:
+ val = ops["-" + optname]
+ if val is None:
+ return False
+ return str(val) == optval or str(val) == optval
+ else:
+ return (ops["--" + option] is not None or
+ ops["-" + option] is not None)
+
+ def match_date(self, date_arg, operator="date"):
+ # The parsed scan's get_date() returns a time.struct_time, so we
+ # need to convert it to a date object
+ from datetime import date, datetime
+ scd = self.parsed_scan.get_date()
+ scan_date = date(scd.tm_year, scd.tm_mon, scd.tm_mday)
+
+ # Check if we have any fuzzy operators ("~") in our string
+ fuzz = 0
+ if "~" in date_arg:
+ # Count 'em, and strip 'em
+ fuzz = date_arg.count("~")
+ date_arg = date_arg.replace("~", "")
+
+ if re.match(r"\d\d\d\d-\d\d-\d\d$", date_arg) is not None:
+ year, month, day = date_arg.split("-")
+ parsed_date = date(int(year), int(month), int(day))
+ elif re.match(r"[-|\+]\d+$", date_arg):
+ # We need to convert from the "-n" format (n days ago) to a date
+ # object (I found this in some old code, don't ask :) )
+ parsed_date = date.fromordinal(
+ date.today().toordinal() + int(date_arg))
+ else:
+ # Fail silently
+ return False
+
+ # Now that we have both the scan date and the user date converted to
+ # date objects, we need to make a comparison based on the operator
+ # (date, after, before).
+ if operator == "date":
+ return abs((scan_date - parsed_date).days) <= fuzz
+ # We ignore fuzziness for after: and before:
+ elif operator == "after":
+ return (scan_date - parsed_date).days >= 0
+ elif operator == "before":
+ return (parsed_date - scan_date).days >= 0
+
+ def match_after(self, date_arg):
+ return self.match_date(date_arg, operator="after")
+
+ def match_before(self, date_arg):
+ return self.match_date(date_arg, operator="before")
+
+ def match_target(self, target):
+ log.debug("Match target: %s" % target)
+
+ for spec in self.parsed_scan.get_targets():
+ if target in spec:
+ return True
+ else:
+ # We search the (rDNS) hostnames list
+ for host in self.parsed_scan.get_hosts():
+ if HostSearch.match_target(host, target):
+ return True
+ return False
+
+ def match_os(self, os):
+ # If you have lots of big scans in your DB (with a lot of hosts
+ # scanned), you're probably better off using the keyword (freetext)
+ # search. Keyword search just greps through the nmap output, while this
+ # function iterates through all parsed OS-related values for every host
+ # in every scan!
+ hosts = self.parsed_scan.get_hosts()
+ for host in hosts:
+ if HostSearch.match_os(host, os):
+ return True
+ return False
+
+ def match_scanned(self, ports):
+ if ports == "":
+ return True
+
+ # Transform a comma-delimited string containing ports into a list
+ ports = [not_empty for not_empty in ports.split(",") if not_empty]
+
+ # Check if they're parsable, if not return False silently
+ for port in ports:
+ if re.match(r"^\d+$", port) is None:
+ return False
+
+ # Make a list of all scanned ports
+ services = []
+ for scaninfo in self.parsed_scan.get_scaninfo():
+ services.append(scaninfo["services"].split(","))
+
+ # These two loops iterate over search ports and over scanned ports. As
+ # soon as the search finds a given port among the scanned ports, it
+ # breaks from the services loop and continues with the next port in the
+ # ports list. If a port isn't found in the services list, the function
+ # immediately returns False.
+ for port in ports:
+ for service in services:
+ if "-" in service and \
+ int(port) >= int(service.split("-")[0]) and \
+ int(port) <= int(service.split("-")[1]):
+ # Port range, and our port was inside
+ break
+ elif port == service:
+ break
+ else:
+ return False
+ else:
+ # The ports loop finished for all ports, which means the search was
+ # successful.
+ return True
+
+ def match_port(self, ports, port_state):
+ log.debug("Match port:%s" % ports)
+
+ # Transform a comma-delimited string containing ports into a list
+ ports = [not_empty for not_empty in ports.split(",") if not_empty]
+
+ for host in self.parsed_scan.get_hosts():
+ for port in ports:
+ if not HostSearch.match_port(
+ host.get_ports(), port, port_state):
+ break
+ else:
+ return True
+ else:
+ return False
+
+ def match_open(self, port):
+ return self.match_port(port, "open")
+
+ def match_filtered(self, port):
+ return self.match_port(port, "filtered")
+
+ def match_closed(self, port):
+ return self.match_port(port, "closed")
+
+ def match_unfiltered(self, port):
+ return self.match_port(port, "unfiltered")
+
+ def match_open_filtered(self, port):
+ return self.match_port(port, "open|filtered")
+
+ def match_closed_filtered(self, port):
+ return self.match_port(port, "closed|filtered")
+
+ def match_service(self, sversion):
+ if sversion == "" or sversion == "*":
+ return True
+
+ for host in self.parsed_scan.get_hosts():
+ if HostSearch.match_service(host, sversion):
+ return True
+ else:
+ return False
+
+ def match_in_route(self, host):
+ if host == "" or host == "*":
+ return True
+ host = host.lower()
+
+ # Since the parser doesn't parse traceroute output, we need to cheat
+ # and look the host up in the Nmap output, in the Traceroute section of
+ # the scan.
+ nmap_out = self.parsed_scan.get_nmap_output()
+ tr_pos = 0
+ traceroutes = [] # A scan holds one traceroute section per host
+ while tr_pos != -1:
+ # Find the beginning and the end of the traceroute section, and
+ # append the substring to the traceroutes list
+ tr_pos = nmap_out.find("TRACEROUTE", tr_pos + 1)
+ tr_end_pos = nmap_out.find("\n\n", tr_pos)
+ if tr_pos != -1:
+ traceroutes.append(nmap_out[tr_pos:tr_end_pos])
+
+ for tr in traceroutes:
+ if host in tr.lower():
+ return True
+ else:
+ return False
+
+
+class SearchDummy(SearchResult):
+ """A dummy search class that returns no results. It is used as a
+ placeholder when SearchDB can't be used."""
+ def get_scan_results(self):
+ return []
+
+
+class SearchDB(SearchResult, object):
+ def __init__(self):
+ SearchResult.__init__(self)
+ log.debug(">>> Getting scan results stored in data base")
+ self.scan_results = []
+ from zenmapCore.UmitDB import UmitDB
+ u = UmitDB()
+
+ for scan in u.get_scans():
+ log.debug(">>> Retrieving result of scans_id %s" % scan.scans_id)
+ log.debug(">>> Nmap xml output: %s" % scan.nmap_xml_output)
+
+ try:
+ buffer = io.StringIO(scan.nmap_xml_output)
+ parsed = NmapParser()
+ parsed.parse(buffer)
+ buffer.close()
+ except Exception as e:
+ log.warning(">>> Error loading scan with ID %u from database: "
+ "%s" % (scan.scans_id, str(e)))
+ else:
+ self.scan_results.append(parsed)
+
+ def get_scan_results(self):
+ return self.scan_results
+
+
+class SearchDir(SearchResult, object):
+ def __init__(self, search_directory, file_extensions=["usr"]):
+ SearchResult.__init__(self)
+ log.debug(">>> SearchDir initialized")
+ self.search_directory = search_directory
+
+ if isinstance(file_extensions, str):
+ self.file_extensions = file_extensions.split(";")
+ elif isinstance(file_extensions, list):
+ self.file_extensions = file_extensions
+ else:
+ raise Exception(
+ "Wrong file extension format! '%s'" % file_extensions)
+
+ log.debug(">>> Getting directory's scan results")
+ self.scan_results = []
+ files = []
+ for ext in self.file_extensions:
+ files += glob(os.path.join(self.search_directory, "*.%s" % ext))
+
+ log.debug(">>> Scan results at selected directory: %s" % files)
+ for scan_file in files:
+ log.debug(">>> Retrieving scan result %s" % scan_file)
+ if os.access(scan_file, os.R_OK) and os.path.isfile(scan_file):
+
+ try:
+ parsed = NmapParser()
+ parsed.parse_file(scan_file)
+ except Exception:
+ pass
+ else:
+ self.scan_results.append(parsed)
+
+ def get_scan_results(self):
+ return self.scan_results
+
+
+class SearchResultTest(unittest.TestCase):
+ class SearchClass(SearchResult):
+ """This class is for use by the unit testing code"""
+ def __init__(self, filenames):
+ SearchResult.__init__(self)
+ self.scan_results = []
+ for filename in filenames:
+ scan = NmapParser()
+ scan.parse_file(filename)
+ self.scan_results.append(scan)
+
+ def get_scan_results(self):
+ return self.scan_results
+
+ def setUp(self):
+ files = ["test/xml_test%d.xml" % no for no in range(1, 13)]
+ self.search_result = self.SearchClass(files)
+
+ def _test_skeleton(self, key, val):
+ results = []
+ search = {key: [val]}
+ for scan in self.search_result.search(**search):
+ results.append(scan)
+ return len(results)
+
+ def test_match_os(self):
+ """Test that checks if the match_os predicate works"""
+ assert(self._test_skeleton('os', 'linux') == 2)
+
+ def test_match_target(self):
+ """Test that checks if the match_target predicate works"""
+ assert(self._test_skeleton('target', 'localhost') == 4)
+
+ def test_match_port_open(self):
+ """Test that checks if the match_open predicate works"""
+ assert(self._test_skeleton('open', '22') == 7)
+
+ def test_match_port_closed(self):
+ """Test that checks if the match_closed predicate works"""
+ assert(self._test_skeleton('open', '22') == 7)
+ assert(self._test_skeleton('closed', '22') == 9)
+
+ def test_match_service(self):
+ """Test that checks if the match_service predicate works"""
+ assert(self._test_skeleton('service', 'apache') == 9)
+ assert(self._test_skeleton('service', 'openssh') == 7)
+
+ def test_match_service_version(self):
+ """Test that checks if the match_service predicate works when """
+ """checking version"""
+ assert(self._test_skeleton('service', '2.0.52') == 7)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/zenmap/zenmapCore/StringPool.py b/zenmap/zenmapCore/StringPool.py
new file mode 100644
index 0000000..3630817
--- /dev/null
+++ b/zenmap/zenmapCore/StringPool.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/
+# *
+# ***************************************************************************/
+
+
+class UniqueStringMap(dict):
+ def __missing__(self, key):
+ self[key] = key
+ return key
+
+UNIQUE_STRING_MAP = UniqueStringMap()
+
+# Return a single unique representation of s (unique as to id),
+# letting s be garbage collected.
+unique = UNIQUE_STRING_MAP.__getitem__
+
+import unittest
+
+
+class StringPoolTest(unittest.TestCase):
+
+ def test_pool(self):
+ source = "Test string. Zenmap. Test string."
+ self.assertIs(unique(source[:12]), unique(source[-12:]))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/zenmap/zenmapCore/TargetList.py b/zenmap/zenmapCore/TargetList.py
new file mode 100644
index 0000000..e7a7172
--- /dev/null
+++ b/zenmap/zenmapCore/TargetList.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/
+# *
+# ***************************************************************************/
+
+from os import access, R_OK, W_OK
+from os.path import dirname
+from zenmapCore.Paths import Path
+
+
+class TargetList(object):
+ def __init__(self):
+ self.temp_list = []
+
+ try:
+ self.target_list_file = Path.target_list
+ except Exception:
+ self.target_list_file = False
+
+ #import pdb; pdb.set_trace()
+ if (self.target_list_file and
+ (access(self.target_list_file, R_OK and W_OK) or
+ access(dirname(self.target_list_file), R_OK and W_OK))):
+ self.using_file = True
+
+ # Recovering saved targets
+ target_file = open(self.target_list_file, "r")
+ self.temp_list = [
+ t for t in target_file.read().split(";")
+ if t != "" and t != "\n"]
+ target_file.close()
+ else:
+ self.using_file = False
+
+ def save(self):
+ if self.using_file:
+ target_file = open(self.target_list_file, "w")
+ target_file.write(";".join(self.temp_list))
+ target_file.close()
+
+ def add_target(self, target):
+ if target in self.temp_list:
+ return
+
+ self.temp_list.append(target)
+ self.save()
+
+ def clean_list(self):
+ del self.temp_list
+ self.temp_list = []
+ self.save()
+
+ def get_target_list(self):
+ t = self.temp_list[:]
+ t.reverse()
+ return t
+
+target_list = TargetList()
+
+if __name__ == "__main__":
+ t = TargetList()
+ print(">>> Getting empty list:", t.get_target_list())
+ print(">>> Adding target 127.0.0.1:", t.add_target("127.0.0.3"))
+ print(">>> Getting target list:", t.get_target_list())
+ del t
diff --git a/zenmap/zenmapCore/UmitConf.py b/zenmap/zenmapCore/UmitConf.py
new file mode 100644
index 0000000..3f7bf0f
--- /dev/null
+++ b/zenmap/zenmapCore/UmitConf.py
@@ -0,0 +1,643 @@
+#!/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 re
+
+from configparser import DuplicateSectionError, NoSectionError, NoOptionError
+from configparser import Error as ConfigParser_Error
+
+from zenmapCore.Paths import Path
+from zenmapCore.UmitLogging import log
+from zenmapCore.UmitConfigParser import UmitConfigParser
+import zenmapCore.I18N # lgtm[py/unused-import]
+
+# This is the global configuration parser object that represents the contents
+# of zenmap.conf. It should be initialized once by the application. Most
+# interaction with the global parser is done by other classes in this file,
+# like SearchConfig, that wrap specific configuration sections.
+config_parser = UmitConfigParser()
+
+# Check if running on Maemo
+MAEMO = False
+try:
+ import hildon
+ MAEMO = True
+except ImportError:
+ pass
+
+
+def is_maemo():
+ return MAEMO
+
+
+class SearchConfig(UmitConfigParser, object):
+ section_name = "search"
+
+ def __init__(self):
+ if not config_parser.has_section(self.section_name):
+ self.create_section()
+
+ def save_changes(self):
+ config_parser.save_changes()
+
+ def create_section(self):
+ config_parser.add_section(self.section_name)
+ self.directory = ""
+ self.file_extension = "xml"
+ self.save_time = "60;days"
+ self.store_results = True
+ self.search_db = True
+
+ def _get_it(self, p_name, default):
+ return config_parser.get(self.section_name, p_name, fallback=default)
+
+ def _set_it(self, p_name, value):
+ config_parser.set(self.section_name, p_name, value)
+
+ def boolean_sanity(self, attr):
+ if attr is True or \
+ attr == "True" or \
+ attr == "true" or \
+ attr == "1":
+ return "True"
+ return "False"
+
+ def get_directory(self):
+ return self._get_it("directory", "")
+
+ def set_directory(self, directory):
+ self._set_it("directory", directory)
+
+ def get_file_extension(self):
+ return self._get_it("file_extension", "xml").split(";")
+
+ def set_file_extension(self, file_extension):
+ if isinstance(file_extension, list):
+ self._set_it("file_extension", ";".join(file_extension))
+ elif isinstance(file_extension, str):
+ self._set_it("file_extension", file_extension)
+
+ def get_save_time(self):
+ return self._get_it("save_time", "60;days").split(";")
+
+ def set_save_time(self, save_time):
+ if isinstance(save_time, list):
+ self._set_it("save_time", ";".join(save_time))
+ elif isinstance(save_time, str):
+ self._set_it("save_time", save_time)
+
+ def get_store_results(self):
+ return self.boolean_sanity(self._get_it("store_results", True))
+
+ def set_store_results(self, store_results):
+ self._set_it("store_results", self.boolean_sanity(store_results))
+
+ def get_search_db(self):
+ return self.boolean_sanity(self._get_it("search_db", True))
+
+ def set_search_db(self, search_db):
+ self._set_it("search_db", self.boolean_sanity(search_db))
+
+ def get_converted_save_time(self):
+ try:
+ return int(self.save_time[0]) * self.time_list[self.save_time[1]]
+ except Exception:
+ # If something goes wrong, return a save time of 60 days
+ return 60 * 60 * 24 * 60
+
+ def get_time_list(self):
+ # Time as key, seconds a value
+ return {"hours": 60 * 60,
+ "days": 60 * 60 * 24,
+ "weeks": 60 * 60 * 24 * 7,
+ "months": 60 * 60 * 24 * 7 * 30,
+ "years": 60 * 60 * 24 * 7 * 30 * 12,
+ "minutes": 60,
+ "seconds": 1}
+
+ directory = property(get_directory, set_directory)
+ file_extension = property(get_file_extension, set_file_extension)
+ save_time = property(get_save_time, set_save_time)
+ store_results = property(get_store_results, set_store_results)
+ search_db = property(get_search_db, set_search_db)
+ converted_save_time = property(get_converted_save_time)
+ time_list = property(get_time_list)
+
+
+class Profile(UmitConfigParser, object):
+ """This class represents not just one profile, but a whole collection of
+ them found in a config file such as scan_profiles.usp. The methods
+ therefore all take an argument that is the name of the profile to work
+ on."""
+
+ def __init__(self, user_profile=None, *args):
+ UmitConfigParser.__init__(self, *args)
+
+ try:
+ if not user_profile:
+ user_profile = Path.scan_profile
+
+ self.read(user_profile)
+ except ConfigParser_Error as e:
+ # No scan profiles found is not a reason to crash.
+ self.add_profile(_("Profiles not found"),
+ command="nmap",
+ description=_("The {} file is missing or corrupted"
+ ).format(user_profile))
+
+ self.attributes = {}
+
+ def _get_it(self, profile, attribute):
+ if self._verify_profile(profile):
+ return self.get(profile, attribute)
+ return ""
+
+ def _set_it(self, profile, attribute, value=''):
+ if self._verify_profile(profile):
+ return self.set(profile, attribute, value)
+
+ def add_profile(self, profile_name, **attributes):
+ """Add a profile with the given name and attributes to the collection
+ of profiles. If a profile with the same name exists, it is not
+ overwritten, and the method returns immediately. The backing file for
+ the profiles is automatically updated."""
+
+ log.debug(">>> Add Profile '%s': %s" % (profile_name, attributes))
+
+ try:
+ self.add_section(profile_name)
+ except DuplicateSectionError:
+ return None
+
+ # Set each of the attributes ("command", "description") in the
+ # ConfigParser.
+ for attr in attributes:
+ self._set_it(profile_name, attr, attributes[attr])
+
+ self.save_changes()
+
+ def remove_profile(self, profile_name):
+ try:
+ self.remove_section(profile_name)
+ except Exception:
+ pass
+ self.save_changes()
+
+ def _verify_profile(self, profile_name):
+ if profile_name not in self.sections():
+ return False
+ return True
+
+
+class WindowConfig(UmitConfigParser, object):
+ section_name = "window"
+
+ default_x = 0
+ default_y = 0
+ default_width = -1
+ default_height = 650
+
+ def __init__(self):
+ if not config_parser.has_section(self.section_name):
+ self.create_section()
+
+ def save_changes(self):
+ config_parser.save_changes()
+
+ def create_section(self):
+ config_parser.add_section(self.section_name)
+ self.x = self.default_x
+ self.y = self.default_y
+ self.width = self.default_width
+ self.height = self.default_height
+
+ def _get_it(self, p_name, default):
+ return config_parser.get(self.section_name, p_name, fallback=default)
+
+ def _set_it(self, p_name, value):
+ config_parser.set(self.section_name, p_name, value)
+
+ def get_x(self):
+ try:
+ value = int(self._get_it("x", self.default_x))
+ except (ValueError, NoOptionError):
+ value = self.default_x
+ except TypeError as e:
+ v = self._get_it("x", self.default_x)
+ log.exception("Trouble parsing x value as int: %s",
+ repr(v), exc_info=e)
+ value = self.default_x
+ return value
+
+ def set_x(self, x):
+ self._set_it("x", "%d" % x)
+
+ def get_y(self):
+ try:
+ value = int(self._get_it("y", self.default_y))
+ except (ValueError, NoOptionError):
+ value = self.default_y
+ except TypeError as e:
+ v = self._get_it("y", self.default_y)
+ log.exception("Trouble parsing y value as int: %s",
+ repr(v), exc_info=e)
+ value = self.default_y
+ return value
+
+ def set_y(self, y):
+ self._set_it("y", "%d" % y)
+
+ def get_width(self):
+ try:
+ value = int(self._get_it("width", self.default_width))
+ except (ValueError, NoOptionError):
+ value = self.default_width
+ except TypeError as e:
+ v = self._get_it("width", self.default_width)
+ log.exception("Trouble parsing width value as int: %s",
+ repr(v), exc_info=e)
+ value = self.default_width
+
+ if not (value >= -1):
+ value = self.default_width
+
+ return value
+
+ def set_width(self, width):
+ self._set_it("width", "%d" % width)
+
+ def get_height(self):
+ try:
+ value = int(self._get_it("height", self.default_height))
+ except (ValueError, NoOptionError):
+ value = self.default_height
+ except TypeError as e:
+ v = self._get_it("height", self.default_height)
+ log.exception("Trouble parsing y value as int: %s",
+ repr(v), exc_info=e)
+ value = self.default_height
+
+ if not (value >= -1):
+ value = self.default_height
+
+ return value
+
+ def set_height(self, height):
+ self._set_it("height", "%d" % height)
+
+ x = property(get_x, set_x)
+ y = property(get_y, set_y)
+ width = property(get_width, set_width)
+ height = property(get_height, set_height)
+
+
+class CommandProfile (Profile, object):
+ """This class is a wrapper around Profile that provides accessors for the
+ attributes of a profile: command and description"""
+ def __init__(self, user_profile=None):
+ Profile.__init__(self, user_profile)
+
+ def get_command(self, profile):
+ command_string = self._get_it(profile, 'command')
+ # Corrupted config file can include multiple commands.
+ # Take the first one.
+ if isinstance(command_string, list):
+ command_string = command_string[0]
+ if not hasattr(command_string, "endswith"):
+ return "nmap"
+ # Old versions of Zenmap used to append "%s" to commands and use that
+ # to substitute the target. Ignore it if present.
+ if command_string.endswith("%s"):
+ command_string = command_string[:-len("%s")]
+ return command_string
+
+ def get_description(self, profile):
+ desc = self._get_it(profile, 'description')
+ if isinstance(desc, list):
+ desc = " ".join(desc)
+ return desc
+
+ def set_command(self, profile, command=''):
+ self._set_it(profile, 'command', command)
+
+ def set_description(self, profile, description=''):
+ self._set_it(profile, 'description', description)
+
+ def get_profile(self, profile_name):
+ return {'profile': profile_name,
+ 'command': self.get_command(profile_name),
+ 'description': self.get_description(profile_name)}
+
+
+class NmapOutputHighlight(object):
+ setts = ["bold", "italic", "underline", "text", "highlight", "regex"]
+
+ def save_changes(self):
+ config_parser.save_changes()
+
+ def __get_it(self, p_name):
+ property_name = "%s_highlight" % p_name
+
+ try:
+ return self.sanity_settings([
+ config_parser.get(
+ property_name, prop, raw=True) for prop in self.setts])
+ except Exception:
+ settings = []
+ prop_settings = self.default_highlights[p_name]
+ settings.append(prop_settings["bold"])
+ settings.append(prop_settings["italic"])
+ settings.append(prop_settings["underline"])
+ settings.append(prop_settings["text"])
+ settings.append(prop_settings["highlight"])
+ settings.append(prop_settings["regex"])
+
+ self.__set_it(p_name, settings)
+
+ return settings
+
+ def __set_it(self, property_name, settings):
+ property_name = "%s_highlight" % property_name
+ settings = self.sanity_settings(list(settings))
+
+ for pos in range(len(settings)):
+ config_parser.set(property_name, self.setts[pos], settings[pos])
+
+ def sanity_settings(self, settings):
+ """This method tries to convert insane settings to sanity ones ;-)
+ If user send a True, "True" or "true" value, for example, it tries to
+ convert then to the integer 1.
+ Same to False, "False", etc.
+
+ Sequence: [bold, italic, underline, text, highlight, regex]
+ """
+ # log.debug(">>> Sanitize %s" % str(settings))
+
+ settings[0] = self.boolean_sanity(settings[0])
+ settings[1] = self.boolean_sanity(settings[1])
+ settings[2] = self.boolean_sanity(settings[2])
+
+ tuple_regex = r"[\(\[]\s?(\d+)\s?,\s?(\d+)\s?,\s?(\d+)\s?[\)\]]"
+ if isinstance(settings[3], str):
+ settings[3] = [
+ int(t) for t in re.findall(tuple_regex, settings[3])[0]
+ ]
+
+ if isinstance(settings[4], str):
+ settings[4] = [
+ int(h) for h in re.findall(tuple_regex, settings[4])[0]
+ ]
+
+ return settings
+
+ def boolean_sanity(self, attr):
+ if attr is True or attr == "True" or attr == "true" or attr == "1":
+ return 1
+ return 0
+
+ def get_date(self):
+ return self.__get_it("date")
+
+ def set_date(self, settings):
+ self.__set_it("date", settings)
+
+ def get_hostname(self):
+ return self.__get_it("hostname")
+
+ def set_hostname(self, settings):
+ self.__set_it("hostname", settings)
+
+ def get_ip(self):
+ return self.__get_it("ip")
+
+ def set_ip(self, settings):
+ self.__set_it("ip", settings)
+
+ def get_port_list(self):
+ return self.__get_it("port_list")
+
+ def set_port_list(self, settings):
+ self.__set_it("port_list", settings)
+
+ def get_open_port(self):
+ return self.__get_it("open_port")
+
+ def set_open_port(self, settings):
+ self.__set_it("open_port", settings)
+
+ def get_closed_port(self):
+ return self.__get_it("closed_port")
+
+ def set_closed_port(self, settings):
+ self.__set_it("closed_port", settings)
+
+ def get_filtered_port(self):
+ return self.__get_it("filtered_port")
+
+ def set_filtered_port(self, settings):
+ self.__set_it("filtered_port", settings)
+
+ def get_details(self):
+ return self.__get_it("details")
+
+ def set_details(self, settings):
+ self.__set_it("details", settings)
+
+ def get_enable(self):
+ enable = True
+ try:
+ enable = config_parser.get("output_highlight", "enable_highlight")
+ except NoSectionError:
+ config_parser.set(
+ "output_highlight", "enable_highlight", str(True))
+
+ if enable == "False" or enable == "0" or enable == "":
+ return False
+ return True
+
+ def set_enable(self, enable):
+ if enable is False or enable == "0" or enable is None or enable == "":
+ config_parser.set(
+ "output_highlight", "enable_highlight", str(False))
+ else:
+ config_parser.set(
+ "output_highlight", "enable_highlight", str(True))
+
+ date = property(get_date, set_date)
+ hostname = property(get_hostname, set_hostname)
+ ip = property(get_ip, set_ip)
+ port_list = property(get_port_list, set_port_list)
+ open_port = property(get_open_port, set_open_port)
+ closed_port = property(get_closed_port, set_closed_port)
+ filtered_port = property(get_filtered_port, set_filtered_port)
+ details = property(get_details, set_details)
+ enable = property(get_enable, set_enable)
+
+ # These settings are made when there is nothing set yet. They set the
+ # "factory" default to highlight colors
+ default_highlights = {
+ "date": {
+ "bold": str(True),
+ "italic": str(False),
+ "underline": str(False),
+ "text": [0, 0, 0],
+ "highlight": [65535, 65535, 65535],
+ "regex": r"\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}\s.{1,4}"},
+ "hostname": {
+ "bold": str(True),
+ "italic": str(True),
+ "underline": str(True),
+ "text": [0, 111, 65535],
+ "highlight": [65535, 65535, 65535],
+ "regex": r"(\w{2,}://)*[\w-]{2,}\.[\w-]{2,}"
+ r"(\.[\w-]{2,})*(/[[\w-]{2,}]*)*"},
+ "ip": {
+ "bold": str(True),
+ "italic": str(False),
+ "underline": str(False),
+ "text": [0, 0, 0],
+ "highlight": [65535, 65535, 65535],
+ "regex": r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"},
+ "port_list": {
+ "bold": str(True),
+ "italic": str(False),
+ "underline": str(False),
+ "text": [0, 1272, 28362],
+ "highlight": [65535, 65535, 65535],
+ "regex": r"PORT\s+STATE\s+SERVICE(\s+VERSION)?[^\n]*"},
+ "open_port": {
+ "bold": str(True),
+ "italic": str(False),
+ "underline": str(False),
+ "text": [0, 41036, 2396],
+ "highlight": [65535, 65535, 65535],
+ "regex": r"\d{1,5}/.{1,5}\s+open\s+.*"},
+ "closed_port": {
+ "bold": str(False),
+ "italic": str(False),
+ "underline": str(False),
+ "text": [65535, 0, 0],
+ "highlight": [65535, 65535, 65535],
+ "regex": r"\d{1,5}/.{1,5}\s+closed\s+.*"},
+ "filtered_port": {
+ "bold": str(False),
+ "italic": str(False),
+ "underline": str(False),
+ "text": [38502, 39119, 0],
+ "highlight": [65535, 65535, 65535],
+ "regex": r"\d{1,5}/.{1,5}\s+filtered\s+.*"},
+ "details": {
+ "bold": str(True),
+ "italic": str(False),
+ "underline": str(True),
+ "text": [0, 0, 0],
+ "highlight": [65535, 65535, 65535],
+ "regex": r"^(\w{2,}[\s]{,3}){,4}:"}
+ }
+
+
+# Retrieve details from zenmap.conf regarding paths subsection
+# (e.g. nmap_command_path) - jurand
+class PathsConfig(object):
+ section_name = "paths"
+
+ # This accounts for missing entries conf file.
+ # Defaults to "nmap" if these errors occur.
+ # NoOptionError, NoSectionError
+ def __get_it(self, p_name, default):
+ try:
+ return config_parser.get(self.section_name, p_name)
+ except (NoOptionError, NoSectionError):
+ log.debug(
+ ">>> Using default \"%s\" for \"%s\"." % (default, p_name))
+ return default
+
+ def __set_it(self, property_name, settings):
+ config_parser.set(self.section_name, property_name, settings)
+
+ def get_nmap_command_path(self):
+ return self.__get_it("nmap_command_path", "nmap")
+
+ def set_nmap_command_path(self, settings):
+ self.__set_it("nmap_command_path", settings)
+
+ def get_ndiff_command_path(self):
+ return self.__get_it("ndiff_command_path", "ndiff")
+
+ def set_ndiff_command_path(self, settings):
+ self.__set_it("ndiff_command_path", settings)
+
+ nmap_command_path = property(get_nmap_command_path, set_nmap_command_path)
+ ndiff_command_path = property(
+ get_ndiff_command_path, set_ndiff_command_path)
+
+
+# Exceptions
+class ProfileNotFound:
+ def __init__(self, profile):
+ self.profile = profile
+
+ def __str__(self):
+ return "No profile named '" + self.profile + "' found!"
+
+
+class ProfileCouldNotBeSaved:
+ def __init__(self, profile):
+ self.profile = profile
+
+ def __str__(self):
+ return "Profile named '" + self.profile + "' could not be saved!"
diff --git a/zenmap/zenmapCore/UmitConfigParser.py b/zenmap/zenmapCore/UmitConfigParser.py
new file mode 100644
index 0000000..05b97a1
--- /dev/null
+++ b/zenmap/zenmapCore/UmitConfigParser.py
@@ -0,0 +1,137 @@
+#!/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 configparser import ConfigParser, DEFAULTSECT, NoOptionError, \
+ NoSectionError
+from zenmapCore.UmitLogging import log
+
+
+class UmitConfigParser(ConfigParser):
+
+ def __init__(self, *args):
+ self.filenames = None
+ self.failed = False
+ ConfigParser.__init__(self, *args)
+
+ def set(self, section, option, value):
+ if not self.has_section(section):
+ self.add_section(section)
+
+ ConfigParser.set(self, section, option, str(value))
+ self.save_changes()
+
+ def read(self, filename):
+ log.debug(">>> Trying to parse: %s" % filename)
+
+ if ConfigParser.read(self, filename):
+ self.filenames = filename
+
+ return self.filenames
+
+ def save_changes(self):
+ if self.filenames:
+ log.debug("saving to %s" % self.filenames)
+ try:
+ with open(self.filenames, 'w') as fp:
+ self.write(fp)
+ except Exception as e:
+ self.failed = e
+ log.error(">>> Can't save to %s: %s" % (self.filenames, e))
+ return
+ self.failed = False
+ else:
+ log.debug(">>> UmitConfigParser can't save changes: no filename")
+
+ def write(self, fp):
+ '''Write alphabetically sorted config files'''
+ if self._defaults:
+ fp.write("[%s]\n" % DEFAULTSECT)
+
+ items = sorted(self._defaults.items())
+
+ for (key, value) in items:
+ fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
+ fp.write("\n")
+
+ sects = sorted(self._sections.keys())
+
+ for section in sects:
+ fp.write("[%s]\n" % section)
+ for (key, value) in self._sections[section].items():
+ if key != "__name__":
+ fp.write("%s = %s\n" %
+ (key, str(value).replace('\n', '\n\t')))
+ fp.write("\n")
+
+
+def test_umit_conf_content(filename):
+ parser = ConfigParser()
+ parser.read(filename)
+
+ # Paths section
+ section = "paths"
+ assert get_or_false(parser, section, "nmap_command_path")
+
+
+def get_or_false(parser, section, option):
+ try:
+ result = parser.get(section, option)
+ return result
+ except NoOptionError:
+ return False
+ except NoSectionError:
+ return False
diff --git a/zenmap/zenmapCore/UmitDB.py b/zenmap/zenmapCore/UmitDB.py
new file mode 100644
index 0000000..2e6c77f
--- /dev/null
+++ b/zenmap/zenmapCore/UmitDB.py
@@ -0,0 +1,324 @@
+#!/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 sqlite3
+import sys
+
+from hashlib import md5
+from time import time
+
+from zenmapCore.Paths import Path
+from zenmapCore.UmitLogging import log
+
+
+umitdb = ""
+
+try:
+ umitdb = Path.db
+except Exception:
+ import os.path
+ from .BasePaths import base_paths
+
+ umitdb = os.path.join(Path.user_config_dir, base_paths["db"])
+ Path.db = umitdb
+
+
+from os.path import exists, dirname
+from os import access, R_OK, W_OK
+
+using_memory = False
+if not exists(umitdb) or \
+ not access(umitdb, R_OK and W_OK) or \
+ not access(dirname(umitdb), R_OK and W_OK):
+ # Tells sqlite to use memory instead of a physics file to avoid crash
+ # and still serve user with most features
+ umitdb = ":memory:"
+ using_memory = True
+
+connection = sqlite3.connect(umitdb)
+
+
+class Table(object):
+ def __init__(self, table_name):
+ self.table_name = table_name
+ self.table_id = "%s_id" % table_name
+
+ self.cursor = connection.cursor()
+
+ def get_item(self, item_name):
+ if self.__getattribute__("_%s" % item_name):
+ return self.__getattribute__("_%s" % item_name)
+
+ sql = "SELECT %s FROM %s WHERE %s_id = %s" % (
+ item_name,
+ self.table_name,
+ self.table_name,
+ self.__getattribute__(self.table_id))
+
+ self.cursor.execute(sql)
+
+ self.__setattr__("_%s" % item_name, self.cursor.fetchall()[0][0])
+ return self.__getattribute__("_%s" % item_name)
+
+ def set_item(self, item_name, item_value):
+ if item_value == self.__getattribute__("_%s" % item_name):
+ return None
+
+ sql = "UPDATE %s SET %s = ? WHERE %s_id = %s" % (
+ self.table_name,
+ item_name,
+ self.table_name,
+ self.__getattribute__(self.table_id))
+ self.cursor.execute(sql, (item_value,))
+ connection.commit()
+ self.__setattr__("_%s" % item_name, item_value)
+
+ def insert(self, **kargs):
+ sql = "INSERT INTO %s ("
+ for k in kargs.keys():
+ sql += k
+ sql += ", "
+
+ sql = sql[:][:-2]
+ sql += ") VALUES ("
+
+ for v in range(len(kargs.values())):
+ sql += "?, "
+
+ sql = sql[:][:-2]
+ sql += ")"
+
+ sql %= self.table_name
+
+ self.cursor.execute(sql, tuple(kargs.values()))
+ connection.commit()
+
+ sql = "SELECT MAX(%s_id) FROM %s;" % (self.table_name, self.table_name)
+ self.cursor.execute(sql)
+ return self.cursor.fetchall()[0][0]
+
+
+class UmitDB(object):
+ def __init__(self):
+ self.cursor = connection.cursor()
+
+ def create_db(self):
+ drop_string = "DROP TABLE scans;"
+
+ try:
+ self.cursor.execute(drop_string)
+ except Exception:
+ connection.rollback()
+ else:
+ connection.commit()
+
+ creation_string = """CREATE TABLE scans (
+ scans_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ scan_name TEXT,
+ nmap_xml_output TEXT,
+ digest TEXT,
+ date INTEGER)"""
+
+ self.cursor.execute(creation_string)
+ connection.commit()
+
+ def add_scan(self, **kargs):
+ return Scans(**kargs)
+
+ def get_scans_ids(self):
+ sql = "SELECT scans_id FROM scans;"
+ self.cursor.execute(sql)
+ return [sid[0] for sid in self.cursor.fetchall()]
+
+ def get_scans(self):
+ scans_ids = self.get_scans_ids()
+ for sid in scans_ids:
+ yield Scans(scans_id=sid)
+
+ def cleanup(self, save_time):
+ log.debug(">>> Cleaning up data base.")
+ log.debug(">>> Removing results older than %s seconds" % save_time)
+ self.cursor.execute("SELECT scans_id FROM scans WHERE date < ?",
+ (time() - save_time,))
+
+ for sid in [sid[0] for sid in self.cursor.fetchall()]:
+ log.debug(">>> Removing results with scans_id %s" % sid)
+ self.cursor.execute("DELETE FROM scans WHERE scans_id = ?",
+ (sid, ))
+
+ connection.commit()
+ log.debug(">>> Data base successfully cleaned up!")
+
+
+class Scans(Table, object):
+ def __init__(self, **kargs):
+ Table.__init__(self, "scans")
+ if "scans_id" in kargs.keys():
+ self.scans_id = kargs["scans_id"]
+ else:
+ log.debug(">>> Creating new scan result entry at data base")
+ fields = ["scan_name", "nmap_xml_output", "date"]
+
+ for k in kargs.keys():
+ if k not in fields:
+ raise Exception(
+ "Wrong table field passed to creation method. "
+ "'%s'" % k)
+
+ if ("nmap_xml_output" not in kargs.keys() or
+ not kargs["nmap_xml_output"]):
+ raise Exception("Can't save result without xml output")
+
+ if not self.verify_digest(
+ md5(kargs["nmap_xml_output"].encode("UTF-8")).hexdigest()):
+ raise Exception("XML output registered already!")
+
+ self.scans_id = self.insert(**kargs)
+
+ def verify_digest(self, digest):
+ self.cursor.execute(
+ "SELECT scans_id FROM scans WHERE digest = ?", (digest, ))
+ result = self.cursor.fetchall()
+ if result:
+ return False
+ return True
+
+ def add_host(self, **kargs):
+ kargs.update({self.table_id: self.scans_id})
+ return Hosts(**kargs)
+
+ def get_hosts(self):
+ sql = "SELECT hosts_id FROM hosts WHERE scans_id= %s" % self.scans_id
+
+ self.cursor.execute(sql)
+ result = self.cursor.fetchall()
+
+ for h in result:
+ yield Hosts(hosts_id=h[0])
+
+ def get_scans_id(self):
+ return self._scans_id
+
+ def set_scans_id(self, scans_id):
+ if scans_id != self._scans_id:
+ self._scans_id = scans_id
+
+ def get_scan_name(self):
+ return self.get_item("scan_name")
+
+ def set_scan_name(self, scan_name):
+ self.set_item("scan_name", scan_name)
+
+ def get_nmap_xml_output(self):
+ return self.get_item("nmap_xml_output")
+
+ def set_nmap_xml_output(self, nmap_xml_output):
+ self.set_item("nmap_xml_output", nmap_xml_output)
+ self.set_item("digest", md5(nmap_xml_output.encode("UTF-8")).hexdigest())
+
+ def get_date(self):
+ return self.get_item("date")
+
+ def set_date(self, date):
+ self.set_item("date", date)
+
+ scans_id = property(get_scans_id, set_scans_id)
+ scan_name = property(get_scan_name, set_scan_name)
+ nmap_xml_output = property(get_nmap_xml_output, set_nmap_xml_output)
+ date = property(get_date, set_date)
+
+ _scans_id = None
+ _scan_name = None
+ _nmap_xml_output = None
+ _date = None
+
+
+######################################################################
+# Verify if data base exists and if it does have the required tables.
+# If something is wrong, re-create table
+def verify_db():
+ cursor = connection.cursor()
+ try:
+ cursor.execute("SELECT scans_id FROM scans WHERE date = 0")
+ except sqlite3.OperationalError:
+ u = UmitDB()
+ u.create_db()
+verify_db()
+
+######################################################################
+
+if __name__ == "__main__":
+ from pprint import pprint
+
+ u = UmitDB()
+
+ #print "Creating Data Base"
+ #u.create_db()
+
+ #print "Creating new scan"
+ #s = u.add_scan(scan_name="Fake scan", nmap_xml_output="", date="007")
+
+ #s = Scans(scans_id=2)
+ #print s.scans_id
+ #print s.scan_name
+ #print s.nmap_xml_output
+ #print s.date
+
+ sql = "SELECT * FROM scans;"
+ u.cursor.execute(sql)
+ print("Scans:", end=' ')
+ pprint(u.cursor.fetchall())
diff --git a/zenmap/zenmapCore/UmitLogging.py b/zenmap/zenmapCore/UmitLogging.py
new file mode 100644
index 0000000..4c38fa4
--- /dev/null
+++ b/zenmap/zenmapCore/UmitLogging.py
@@ -0,0 +1,97 @@
+#!/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 logging import Logger, StreamHandler, Formatter
+from zenmapCore.Name import APP_DISPLAY_NAME
+from zenmapCore.UmitOptionParser import option_parser
+from zenmapCore.DelayedObject import DelayedObject
+
+
+class Log(Logger, object):
+ def __init__(self, name, level=0):
+ if level == 0:
+ level = option_parser.get_verbose()
+ Logger.__init__(self, name, level)
+ self.formatter = self.format
+
+ handler = StreamHandler()
+ handler.setFormatter(self.formatter)
+
+ self.addHandler(handler)
+
+ def get_formatter(self):
+ return self.__formatter
+
+ def set_formatter(self, fmt):
+ self.__formatter = Formatter(fmt)
+
+ format = "%(levelname)s - %(asctime)s - %(message)s"
+
+ formatter = property(get_formatter, set_formatter, doc="")
+ __formatter = Formatter(format)
+
+
+# Import this!
+log = DelayedObject(Log, APP_DISPLAY_NAME)
+
+if __name__ == '__main__':
+ log.debug("Debug Message")
+ log.info("Info Message")
+ log.warning("Warning Message")
+ log.error("Error Message")
+ log.critical("Critical Message")
diff --git a/zenmap/zenmapCore/UmitOptionParser.py b/zenmap/zenmapCore/UmitOptionParser.py
new file mode 100644
index 0000000..7a01754
--- /dev/null
+++ b/zenmap/zenmapCore/UmitOptionParser.py
@@ -0,0 +1,202 @@
+#!/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 optparse import OptionParser
+from zenmapCore.Name import NMAP_DISPLAY_NAME
+from zenmapCore.Version import VERSION
+import zenmapCore.I18N # lgtm[py/unused-import]
+from zenmapCore.BasePaths import base_paths
+from zenmapCore.DelayedObject import DelayedObject
+
+
+class UmitOptionParser(OptionParser):
+ def __init__(self, args=False):
+ OptionParser.__init__(self, version="%%prog %s" % VERSION)
+
+ self.set_usage("%prog [options] [result files]")
+
+ self.add_option("--confdir",
+ default=base_paths["user_config_dir"],
+ dest="confdir",
+ metavar="DIR",
+ help=_("\
+Use DIR as the user configuration directory. Default: %default"))
+
+ ## Open Scan Results (GUI)
+ ### Run, opening the specified scan result file, which should be
+ ### a nmap XML output file.
+ ### This option should be verified if there is no options, and user
+ ### specified some positional arguments, which should be considered as
+ ### scan result files.
+ self.add_option("-f", "--file",
+ default=[],
+ action="append",
+ type="string",
+ dest="result_files",
+ help=_("Specify a scan result file in Nmap XML output \
+format. Can be used more than once to specify several \
+scan result files."))
+
+ ## Run nmap with args (GUI)
+ ### Open and run nmap with specified args. The positional
+ ### args should be used to feed the nmap command
+ self.add_option("-n", "--nmap",
+ default=[],
+ action="callback",
+ callback=self.__nmap_callback,
+ help=_("Run %s with the specified args."
+ ) % NMAP_DISPLAY_NAME)
+
+ ## Execute a profile against a target (GUI)
+ ### Positional args should be taken as targets to feed this scan
+ self.add_option("-p", "--profile",
+ default="",
+ action="store",
+ help=_("Begin with the specified profile \
+selected. If combined with the -t (--target) option, \
+automatically run the profile against the specified target."))
+
+ ## Targets (GUI)
+ ### Specify a target to be used along with other command line option
+ ### or simply opens with the first tab target field filled with
+ ### the target specified with this option
+ self.add_option("-t", "--target",
+ default=False,
+ action="store",
+ help=_("Specify a target to be used along with other \
+options. If specified alone, open with the target field filled with the \
+specified target"))
+
+ ## Verbosity
+ self.add_option("-v", "--verbose",
+ default=0,
+ action="count",
+ help=_("Increase verbosity of the output. May be \
+used more than once to get even more verbosity"))
+
+ # Parsing options and arguments
+ if args:
+ self.options, self.args = self.parse_args(args)
+ else:
+ self.options, self.args = self.parse_args()
+
+ def __nmap_callback(self, option, opt_str, value, parser):
+ nmap_args = []
+ # Iterate over next arguments that were passed at the command line
+ # that wasn't parsed yet.
+ while parser.rargs:
+ # Store the next argument in a specific list
+ nmap_args.append(parser.rargs[0])
+
+ # Remove the added argument from rargs to avoid its later
+ # parsing by optparse
+ del parser.rargs[0]
+
+ # Set the variable nmap at parser.values, so you may call option.nmap
+ # and have the nmap_args as result
+ setattr(parser.values, "nmap", nmap_args)
+
+ def get_confdir(self):
+ return self.options.confdir
+
+ def get_nmap(self):
+ """Return a list of nmap arguments or False if this option was not
+ called by the user"""
+
+ try:
+ nmap = self.options.nmap
+ if nmap:
+ return nmap
+ except AttributeError:
+ return False
+
+ def get_profile(self):
+ """Return a string with the profile name, or False if no profile
+ option was specified by the user"""
+ if self.options.profile != "":
+ return self.options.profile
+ return False
+
+ def get_target(self):
+ """Returns a string with the target specified, or False if this option
+ was not called by the user"""
+ return self.options.target
+
+ def get_open_results(self):
+ """Returns a list of strings with the name of the files specified with
+ the -f (--file) option and every positional argument."""
+ files = []
+ # Add arguments given with -f.
+ if self.options.result_files:
+ files = self.options.result_files[:]
+ # Add any other arguments.
+ files += self.args
+ return files
+
+ def get_verbose(self):
+ """Returns an integer representing the verbosity level of the
+ application. Verbosity level starts in 40, which means that only
+ messages above the ERROR level are going to be reported at the output.
+ As this value gets lower, the verbosity increases.
+ """
+ return 40 - (self.options.verbose * 10)
+
+option_parser = DelayedObject(UmitOptionParser)
+
+if __name__ == "__main__":
+ opt = UmitOptionParser()
+ options, args = opt.parse_args()
diff --git a/zenmap/zenmapCore/Version.py b/zenmap/zenmapCore/Version.py
new file mode 100644
index 0000000..b3fede4
--- /dev/null
+++ b/zenmap/zenmapCore/Version.py
@@ -0,0 +1 @@
+VERSION = "7.94SVN"
diff --git a/zenmap/zenmapCore/__init__.py b/zenmap/zenmapCore/__init__.py
new file mode 100644
index 0000000..cf74df4
--- /dev/null
+++ b/zenmap/zenmapCore/__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/
+# *
+# ***************************************************************************/