diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:42:04 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:42:04 +0000 |
commit | 0d47952611198ef6b1163f366dc03922d20b1475 (patch) | |
tree | 3d840a3b8c0daef0754707bfb9f5e873b6b1ac13 /zenmap/zenmapCore | |
parent | Initial commit. (diff) | |
download | nmap-upstream.tar.xz nmap-upstream.zip |
Adding upstream version 7.94+git20230807.3be01efb1+dfsg.upstream/7.94+git20230807.3be01efb1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
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/ +# * +# ***************************************************************************/ |