1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 04:31:29 +00:00
Files
nmap/zenmap/zenmapCore/NmapCommand.py

272 lines
11 KiB
Python

#!/usr/bin/env python3
# ***********************IMPORTANT NMAP LICENSE TERMS************************
# *
# * The Nmap Security Scanner is (C) 1996-2025 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-", encoding="utf-8",
errors='backslashreplace')
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, encoding="utf-8",
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))