#!/usr/bin/env python # -*- coding: utf-8 -*- """ nmap.py - version and date, see below Source code : https://bitbucket.org/xael/python-nmap Author : * Alexandre Norman - norman at xael.org Contributors: * Steve 'Ashcrow' Milner - steve at gnulinux.net * Brian Bustin - brian at bustin.us * old.schepperhand * Johan Lundberg * Thomas D. maaaaz * Robert Bost * David Peltier * Ed Jones Licence: GPL v3 or any later version for python-nmap This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program 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. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ************** IMPORTANT NOTE ************** The Nmap Security Scanner used by python-nmap is distributed under it's own licence that you can find at https://svn.nmap.org/nmap/COPYING Any redistribution of python-nmap along with the Nmap Security Scanner must conform to the Nmap Security Scanner licence """ import csv import io import os import re import shlex import subprocess import sys from multiprocessing import Process from xml.etree import ElementTree as ET __author__ = "Alexandre Norman (norman@xael.org)" __version__ = "0.7.1" __last_modification__ = "2021.10.26" ############################################################################ class PortScanner(object): """ PortScanner class allows to use nmap from python """ def __init__( self, nmap_search_path=( "nmap", "/usr/bin/nmap", "/usr/local/bin/nmap", "/sw/bin/nmap", "/opt/local/bin/nmap", ), ): """ Initialize PortScanner module * detects nmap on the system and nmap version * may raise PortScannerError exception if nmap is not found in the path :param nmap_search_path: tupple of string where to search for nmap executable. Change this if you want to use a specific version of nmap. :returns: nothing """ self._nmap_path = "" # nmap path self._scan_result = {} self._nmap_version_number = 0 # nmap version number self._nmap_subversion_number = 0 # nmap subversion number self._nmap_last_output = "" # last full ascii nmap output is_nmap_found = False # true if we have found nmap self.__process = None # regex used to detect nmap (http or https) regex = re.compile(r"Nmap version [0-9]*\.[0-9]*[^ ]* \( http(|s)://.* \)") # launch 'nmap -V', we wait after # > 'Nmap version 5.0 ( http://nmap.org )' # This is for Mac OSX. When idle3 is launched from the finder, PATH is not set so nmap was not found for nmap_path in nmap_search_path: try: if ( sys.platform.startswith("freebsd") or sys.platform.startswith("linux") or sys.platform.startswith("darwin") ): p = subprocess.Popen( [nmap_path, "-V"], bufsize=10000, stdout=subprocess.PIPE, close_fds=True, ) else: p = subprocess.Popen( [nmap_path, "-V"], bufsize=10000, stdout=subprocess.PIPE ) except OSError: pass else: self._nmap_path = nmap_path # save path break else: raise PortScannerError( f"nmap program was not found in path. PATH is : {os.getenv('PATH')}" ) self._nmap_last_output = bytes.decode(p.communicate()[0]) # sav stdout for line in self._nmap_last_output.split(os.linesep): if regex.match(line) is not None: is_nmap_found = True # Search for version number regex_version = re.compile("[0-9]+") regex_subversion = re.compile(r"\.[0-9]+") rv = regex_version.search(line) rsv = regex_subversion.search(line) if rv is not None and rsv is not None: # extract version/subversion self._nmap_version_number = int(line[rv.start() : rv.end()]) self._nmap_subversion_number = int( line[rsv.start() + 1 : rsv.end()] ) break if not is_nmap_found: raise PortScannerError("nmap program was not found in path") return def get_nmap_last_output(self): """ Returns the last text output of nmap in raw text this may be used for debugging purpose :returns: string containing the last text output of nmap in raw text """ return self._nmap_last_output def nmap_version(self): """ returns nmap version if detected (int version, int subversion) or (0, 0) if unknown :returns: (nmap_version_number, nmap_subversion_number) """ return (self._nmap_version_number, self._nmap_subversion_number) def listscan(self, hosts="127.0.0.1"): """ do not scan but interpret target hosts and return a list a hosts """ assert ( type(hosts) is str ), f"Wrong type for [hosts], should be a string [was {type(hosts)}]" output = self.scan(hosts, arguments="-sL") # Test if host was IPV6 if ( "scaninfo" in output["nmap"] and "error" in output["nmap"]["scaninfo"] and len(output["nmap"]["scaninfo"]["error"]) > 0 and "looks like an IPv6 target specification" in output["nmap"]["scaninfo"]["error"][0] ): # noqa self.scan(hosts, arguments="-sL -6") return self.all_hosts() def scan( # NOQA: CFQ001, C901 self, hosts="127.0.0.1", ports=None, arguments="-sV", sudo=False, timeout=0 ): """ Scan given hosts May raise PortScannerError exception if nmap output was not xml Test existance of the following key to know if something went wrong : ['nmap']['scaninfo']['error'] If not present, everything was ok. :param hosts: string for hosts as nmap use it 'scanme.nmap.org' or '198.116.0-255.1-127' or '216.163.128.20/20' :param ports: string for ports as nmap use it '22,53,110,143-4564' :param arguments: string of arguments for nmap '-sU -sX -sC' :param sudo: launch nmap with sudo if True :param timeout: int, if > zero, will terminate scan after seconds, otherwise will wait indefintely :returns: scan_result as dictionnary """ if sys.version_info[0] == 2: assert type(hosts) in ( str, ), f"Wrong type for [hosts], should be a string [was {type(hosts)}]" assert type(ports) in ( str, type(None), ), f"Wrong type for [ports], should be a string [was {type(ports)}]" assert type(arguments) in ( str, ), f"Wrong type for [arguments], should be a string [was {type(arguments)}]" else: assert ( type(hosts) is str ), f"Wrong type for [hosts], should be a string [was {type(hosts)}]" assert type(ports) in ( str, type(None), ), f"Wrong type for [ports], should be a string [was {type(ports)}]" assert ( type(arguments) is str ), f"Wrong type for [arguments], should be a string [was {type(arguments)}]" for redirecting_output in ["-oX", "-oA"]: assert ( redirecting_output not in arguments ), "Xml output can't be redirected from command line.\nYou can access it after a scan using:\nnmap.nm.get_nmap_last_output()" # noqa h_args = shlex.split(hosts) f_args = shlex.split(arguments) # Launch scan args = ( [self._nmap_path, "-oX", "-"] + h_args + ["-p", ports] * (ports is not None) + f_args ) if sudo: args = ["sudo"] + args p = subprocess.Popen( args, bufsize=100000, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # wait until finished # get output # Terminate after user timeout if timeout == 0: (self._nmap_last_output, nmap_err) = p.communicate() else: try: (self._nmap_last_output, nmap_err) = p.communicate(timeout=timeout) except subprocess.TimeoutExpired: p.kill() raise PortScannerTimeout("Timeout from nmap process") nmap_err = bytes.decode(nmap_err) # If there was something on stderr, there was a problem so abort... in # fact not always. As stated by AlenLPeacock : # This actually makes python-nmap mostly unusable on most real-life # networks -- a particular subnet might have dozens of scannable hosts, # but if a single one is unreachable or unroutable during the scan, # nmap.scan() returns nothing. This behavior also diverges significantly # from commandline nmap, which simply stderrs individual problems but # keeps on trucking. nmap_err_keep_trace = [] nmap_warn_keep_trace = [] if len(nmap_err) > 0: regex_warning = re.compile("^Warning: .*", re.IGNORECASE) for line in nmap_err.split(os.linesep): if len(line) > 0: rgw = regex_warning.search(line) if rgw is not None: nmap_warn_keep_trace.append(line + os.linesep) else: nmap_err_keep_trace.append(nmap_err) return self.analyse_nmap_xml_scan( nmap_xml_output=self._nmap_last_output, nmap_err=nmap_err, nmap_err_keep_trace=nmap_err_keep_trace, nmap_warn_keep_trace=nmap_warn_keep_trace, ) def analyse_nmap_xml_scan( # NOQA: CFQ001, C901 self, nmap_xml_output=None, nmap_err="", nmap_err_keep_trace="", nmap_warn_keep_trace="", ): """ Analyses NMAP xml scan ouput May raise PortScannerError exception if nmap output was not xml Test existance of the following key to know if something went wrong : ['nmap']['scaninfo']['error'] If not present, everything was ok. :param nmap_xml_output: xml string to analyse :returns: scan_result as dictionnary """ # nmap xml output looks like : # # #
# # # # # # # # # # # # #