python-libnmap-0.7.0/0000775000175000017500000000000012664653634015260 5ustar ronaldronald00000000000000python-libnmap-0.7.0/setup.py0000664000175000017500000000213312664652760016770 0ustar ronaldronald00000000000000# -*- coding: utf-8 -*- from distutils.core import setup with open("README.rst") as rfile: long_description = rfile.read() setup( name='python-libnmap', version='0.7.0', author='Ronald Bister', author_email='mini.pelle@gmail.com', packages=['libnmap', 'libnmap.plugins', 'libnmap.objects'], url='http://pypi.python.org/pypi/python-libnmap/', license='Creative Common "Attribution" license (CC-BY) v3', description=('Python NMAP library enabling you to start async nmap tasks, ' 'parse and compare/diff scan results'), long_description=long_description, classifiers=["Development Status :: 5 - Production/Stable", "Environment :: Console", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Topic :: System :: Networking"] ) python-libnmap-0.7.0/LICENSE.txt0000664000175000017500000000226412664652334017103 0ustar ronaldronald00000000000000This code is licensed under Creative Common "Attribution" license (CC-BY: http://creativecommons.org/licenses/by/3.0/). You are free to: - Share: to copy, distribute and transmit the work - Remix: to adapt the work - and make commercial use of the work Under the following conditions: - Attribution: You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work). With the understanding that: Waiver: Any of the above conditions can be waived if you get permission from the copyright holder. Public Domain: Where the work or any of its elements is in the public domain under applicable law, that status is in no way affected by the license. Other Rights: In no way are any of the following rights affected by the license: Your fair dealing or fair use rights, or other applicable copyright exceptions and limitations; The author's moral rights; Rights other persons may have either in the work itself or in how the work is used, such as publicity or privacy rights. The full text is available here: http://creativecommons.org/licenses/by/3.0/legalcode python-libnmap-0.7.0/libnmap/0000775000175000017500000000000012664653634016702 5ustar ronaldronald00000000000000python-libnmap-0.7.0/libnmap/diff.py0000664000175000017500000000552012664652334020162 0ustar ronaldronald00000000000000# -*- coding: utf-8 -*- class DictDiffer(object): """ Calculate the difference between two dictionaries as: (1) items added (2) items removed (3) keys same in both but changed values (4) keys same in both and unchanged values """ def __init__(self, current_dict, past_dict): self.current_dict = current_dict self.past_dict = past_dict self.set_current = set(current_dict.keys()) self.set_past = set(past_dict.keys()) self.intersect = self.set_current.intersection(self.set_past) def added(self): return self.set_current - self.intersect def removed(self): return self.set_past - self.intersect def changed(self): return (set(o for o in self.intersect if self.past_dict[o] != self.current_dict[o])) def unchanged(self): return (set(o for o in self.intersect if self.past_dict[o] == self.current_dict[o])) class NmapDiff(DictDiffer): """ NmapDiff compares two objects of same type to enable the user to check: - what has changed - what has been added - what has been removed - what was kept unchanged NmapDiff inherit from DictDiffer which makes the actual comparaison. The different methods from DictDiffer used by NmapDiff are the following: - NmapDiff.changed() - NmapDiff.added() - NmapDiff.removed() - NmapDiff.unchanged() Each of the returns a python set() of key which have changed in the compared objects. To check the different keys that could be returned, refer to the get_dict() method of the objects you which to compare (i.e: libnmap.objects.NmapHost, NmapService,...). """ def __init__(self, nmap_obj1, nmap_obj2): """ Constructor of NmapDiff: - Checks if the two objects are of the same class - Checks if the objects are "comparable" via a call to id() (dirty) - Inherits from DictDiffer and """ if(nmap_obj1.__class__ != nmap_obj2.__class__ or nmap_obj1.id != nmap_obj2.id): raise NmapDiffException("Comparing objects with non-matching id") self.object1 = nmap_obj1.get_dict() self.object2 = nmap_obj2.get_dict() DictDiffer.__init__(self, self.object1, self.object2) def __repr__(self): return ("added: [{0}] -- changed: [{1}] -- " "unchanged: [{2}] -- removed [{3}]".format(self.added(), self.changed(), self.unchanged(), self.removed())) class NmapDiffException(Exception): def __init__(self, msg): self.msg = msg python-libnmap-0.7.0/libnmap/parser.py0000664000175000017500000006331112664652334020550 0ustar ronaldronald00000000000000# -*- coding: utf-8 -*- try: import xml.etree.cElementTree as ET except ImportError: import xml.etree.ElementTree as ET from libnmap.objects import NmapHost, NmapService, NmapReport class NmapParser(object): @classmethod def parse(cls, nmap_data=None, data_type='XML', incomplete=False): """ Generic class method of NmapParser class. The data to be parsed does not need to be a complete nmap scan report. You can possibly give ... or XML tags. :param nmap_data: any portion of nmap scan result. \ nmap_data should always be a string representing a part \ or a complete nmap scan report. :type nmap_data: string :param data_type: specifies the type of data to be parsed. :type data_type: string ("XML"|"JSON"|"YAML"). :param incomplete: enable you to parse interrupted nmap scans \ and/or incomplete nmap xml blocks by adding a at \ the end of the scan. :type incomplete: boolean As of today, only XML parsing is supported. :return: NmapObject (NmapHost, NmapService or NmapReport) """ nmapobj = None if data_type == "XML": nmapobj = cls._parse_xml(nmap_data, incomplete) else: raise NmapParserException("Unknown data type provided. " "Please check documentation for " "supported data types.") return nmapobj @classmethod def _parse_xml(cls, nmap_data=None, incomplete=False): """ Protected class method used to process a specific data type. In this case: XML. This method is called by cls.parse class method and receives nmap scan results data (in XML). :param nmap_data: any portion of nmap scan result can be given \ as argument. nmap_data should always be a string representing \ a part or a complete nmap scan report. :type nmap_data: string This method checks which portion of a nmap scan is given \ as argument. It could be: 1. a full nmap scan report; 2. a scanned host: tag in a nmap scan report 3. a scanned service: tag 4. a list of hosts: tag (TODO) 5. a list of ports: tag :param incomplete: enable you to parse interrupted nmap scans \ and/or incomplete nmap xml blocks by adding a at \ the end of the scan. :type incomplete: boolean :return: NmapObject (NmapHost, NmapService or NmapReport) \ or a list of NmapObject """ if not nmap_data: raise NmapParserException("No report data to parse: please " "provide a valid XML nmap report") elif not isinstance(nmap_data, str): raise NmapParserException("wrong nmap_data type given as " "argument: cannot parse data") if incomplete is True: nmap_data += "" try: root = ET.fromstring(nmap_data) except: raise NmapParserException("Wrong XML structure: cannot parse data") nmapobj = None if root.tag == 'nmaprun': nmapobj = cls._parse_xml_report(root) elif root.tag == 'host': nmapobj = cls._parse_xml_host(root) elif root.tag == 'ports': nmapobj = cls._parse_xml_ports(root) elif root.tag == 'port': nmapobj = cls._parse_xml_port(root) else: raise NmapParserException("Unpexpected data structure for XML " "root node") return nmapobj @classmethod def _parse_xml_report(cls, root=None): """ This method parses out a full nmap scan report from its XML root node: . :param root: Element from xml.ElementTree (top of XML the document) :type root: Element :return: NmapReport object """ nmap_scan = {'_nmaprun': {}, '_scaninfo': {}, '_hosts': [], '_runstats': {}} if root is None: raise NmapParserException("No root node provided to parse XML " "report") nmap_scan['_nmaprun'] = cls.__format_attributes(root) for el in root: if el.tag == 'scaninfo': nmap_scan['_scaninfo'] = cls.__parse_scaninfo(el) elif el.tag == 'host': nmap_scan['_hosts'].append(cls._parse_xml_host(el)) elif el.tag == 'runstats': nmap_scan['_runstats'] = cls.__parse_runstats(el) # else: # print "struct pparse unknown attr: {0} value: {1}".format( # el.tag, # el.get(el.tag)) return NmapReport(nmap_scan) @classmethod def parse_fromstring(cls, nmap_data, data_type="XML", incomplete=False): """ Call generic cls.parse() method and ensure that a string is \ passed on as argument. If not, an exception is raised. :param nmap_data: Same as for parse(), any portion of nmap scan. \ Reports could be passed as argument. Data type _must_ be a string. :type nmap_data: string :param data_type: Specifies the type of data passed on as argument. :param incomplete: enable you to parse interrupted nmap scans \ and/or incomplete nmap xml blocks by adding a at \ the end of the scan. :type incomplete: boolean :return: NmapObject """ if not isinstance(nmap_data, str): raise NmapParserException("bad argument type for " "xarse_fromstring(): should be a string") return cls.parse(nmap_data, data_type, incomplete) @classmethod def parse_fromfile(cls, nmap_report_path, data_type="XML", incomplete=False): """ Call generic cls.parse() method and ensure that a correct file \ path is given as argument. If not, an exception is raised. :param nmap_data: Same as for parse(). \ Any portion of nmap scan reports could be passed as argument. \ Data type _must be a valid path to a file containing \ nmap scan results. :param data_type: Specifies the type of serialization in the file. :param incomplete: enable you to parse interrupted nmap scans \ and/or incomplete nmap xml blocks by adding a at \ the end of the scan. :type incomplete: boolean :return: NmapObject """ try: with open(nmap_report_path, 'r') as fileobj: fdata = fileobj.read() rval = cls.parse(fdata, data_type, incomplete) except IOError: raise return rval @classmethod def parse_fromdict(cls, rdict): """ Strange method which transforms a python dict \ representation of a NmapReport and turns it into an \ NmapReport object. \ Needs to be reviewed and possibly removed. :param rdict: python dict representation of an NmapReport :type rdict: dict :return: NmapReport """ nreport = {} if list(rdict.keys())[0] == '__NmapReport__': r = rdict['__NmapReport__'] nreport['_runstats'] = r['_runstats'] nreport['_scaninfo'] = r['_scaninfo'] nreport['_nmaprun'] = r['_nmaprun'] hlist = [] for h in r['_hosts']: slist = [] for s in h['__NmapHost__']['_services']: cname = '__NmapService__' slist.append(NmapService(portid=s[cname]['_portid'], protocol=s[cname]['_protocol'], state=s[cname]['_state'], owner=s[cname]['_owner'], service=s[cname]['_service'])) nh = NmapHost(starttime=h['__NmapHost__']['_starttime'], endtime=h['__NmapHost__']['_endtime'], address=h['__NmapHost__']['_address'], status=h['__NmapHost__']['_status'], hostnames=h['__NmapHost__']['_hostnames'], extras=h['__NmapHost__']['_extras'], services=slist) hlist.append(nh) nreport['_hosts'] = hlist nmapobj = NmapReport(nreport) return nmapobj @classmethod def __parse_scaninfo(cls, scaninfo_data): """ Private method parsing a portion of a nmap scan result. Receives a XML tag. :param scaninfo_data: XML tag from a nmap scan :type scaninfo_data: xml.ElementTree.Element or a string :return: python dict representing the XML scaninfo tag """ xelement = cls.__format_element(scaninfo_data) return cls.__format_attributes(xelement) @classmethod def _parse_xml_host(cls, scanhost_data): """ Protected method parsing a portion of a nmap scan result. Receives a XML tag representing a scanned host with its services. :param scaninfo_data: XML tag from a nmap scan :type scaninfo_data: xml.ElementTree.Element or a string :return: NmapHost object """ xelement = cls.__format_element(scanhost_data) _host_header = cls.__format_attributes(xelement) _hostnames = [] _services = [] _status = {} _addresses = [] _host_extras = {} extra_tags = ['uptime', 'distance', 'tcpsequence', 'ipidsequence', 'tcptssequence', 'times'] for xh in xelement: if xh.tag == 'hostnames': for hostname in cls.__parse_hostnames(xh): _hostnames.append(hostname) elif xh.tag == 'ports': ports_dict = cls._parse_xml_ports(xh) for port in ports_dict['ports']: _services.append(port) _host_extras['extraports'] = ports_dict['extraports'] elif xh.tag == 'status': _status = cls.__format_attributes(xh) elif xh.tag == 'address': _addresses.append(cls.__format_attributes(xh)) elif xh.tag == 'os': _os_extra = cls.__parse_os_fingerprint(xh) _host_extras.update({'os': _os_extra}) elif xh.tag == 'hostscript': _host_scripts = cls.__parse_host_scripts(xh) _host_extras.update({'hostscript': _host_scripts}) elif xh.tag in extra_tags: _host_extras[xh.tag] = cls.__format_attributes(xh) # else: # print "struct host unknown attr: %s value: %s" % # (h.tag, h.get(h.tag)) _stime = '' _etime = '' if 'starttime' in _host_header: _stime = _host_header['starttime'] if 'endtime' in _host_header: _etime = _host_header['endtime'] nhost = NmapHost(_stime, _etime, _addresses, _status, _hostnames, _services, _host_extras) return nhost @classmethod def __parse_hostnames(cls, scanhostnames_data): """ Private method parsing the hostnames list within a XML tag. :param scanhostnames_data: XML tag from a nmap scan :type scanhostnames_data: xml.ElementTree.Element or a string :return: list of hostnames """ xelement = cls.__format_element(scanhostnames_data) hostnames = [] for hname in xelement: if hname.tag == 'hostname': hostnames.append(hname.get('name')) return hostnames @classmethod def _parse_xml_ports(cls, scanports_data): """ Protected method parsing the list of scanned services from a targeted host. This protected method cannot be called directly with a string. A tag can be directly passed to parse() and the below method will be called and return a list of nmap scanned services. :param scanports_data: XML tag from a nmap scan :type scanports_data: xml.ElementTree.Element or a string :return: list of NmapService """ xelement = cls.__format_element(scanports_data) rdict = {'ports': [], 'extraports': None} for xservice in xelement: if xservice.tag == 'port': nport = cls._parse_xml_port(xservice) rdict['ports'].append(nport) elif xservice.tag == 'extraports': extraports = cls.__parse_extraports(xservice) rdict['extraports'] = extraports # else: # print "struct port unknown attr: %s value: %s" % # (h.tag, h.get(h.tag)) return rdict @classmethod def _parse_xml_port(cls, scanport_data): """ Protected method parsing a scanned service from a targeted host. This protected method cannot be called directly. A tag can be directly passed to parse() and the below method will be called and return a NmapService object representing the state of the service. :param scanport_data: XML tag from a nmap scan :type scanport_data: xml.ElementTree.Element or a string :return: NmapService """ xelement = cls.__format_element(scanport_data) _port = cls.__format_attributes(xelement) _portid = _port['portid'] if 'portid' in _port else None _protocol = _port['protocol'] if 'protocol' in _port else None _state = None _service = None _owner = None _service_scripts = [] _service_extras = {} for xport in xelement: if xport.tag == 'state': _state = cls.__format_attributes(xport) elif xport.tag == 'service': _service = cls.__parse_service(xport) elif xport.tag == 'owner': _owner = cls.__format_attributes(xport) elif xport.tag == 'script': _script_dict = cls.__parse_script(xport) _service_scripts.append(_script_dict) _service_extras['scripts'] = _service_scripts if(_portid is None or _protocol is None or _state is None): raise NmapParserException("XML tag is incomplete. One " "of the following tags is missing: " "portid, protocol or state or tag.") nport = NmapService(_portid, _protocol, _state, _service, _owner, _service_extras) return nport @classmethod def __parse_service(cls, xserv): """ Parse tag to manage CPE object """ _service = cls.__format_attributes(xserv) _cpelist = [] for _servnode in xserv: if _servnode.tag == 'cpe': _cpe_string = _servnode.text _cpelist.append(_cpe_string) _service['cpelist'] = _cpelist return _service @classmethod def __parse_extraports(cls, extraports_data): """ Private method parsing the data from extra scanned ports. X extraports were in state "closed" server returned "conn-refused" tag: :param extraports_data: XML data for extraports :type extraports_data: xml.ElementTree.Element or a string :return: python dict with following keys: state, count, reason """ rdict = {'state': '', 'count': '', 'reasons': []} xelement = cls.__format_element(extraports_data) extraports_dict = cls.__format_attributes(xelement) if 'state' in extraports_dict: rdict['state'] = extraports_dict if 'count' in extraports_dict: rdict['count'] = extraports_dict for xelt in xelement: if xelt.tag == 'extrareasons': extrareasons_dict = cls.__format_attributes(xelt) rdict['reasons'].append(extrareasons_dict) return rdict @classmethod def __parse_script(cls, script_data): """ Private method parsing the data from NSE scripts output :param script_data: portion of XML describing the results of the script data :type script_data: xml.ElementTree.Element or a string :return: python dict holding scripts output """ _script_dict = cls.__format_attributes(script_data) _elt_dict = {} for script_elem in script_data: if script_elem.tag == 'elem': _elt_dict.update({script_elem.get('key'): script_elem.text}) elif script_elem.tag == 'table': tdict = {} for telem in script_elem: # Handle duplicate element keys tkey = telem.get('key') if tkey in tdict: if not isinstance(tdict[tkey], list): tdict[tkey] = [tdict[tkey], ] tdict[tkey].append(telem.text) else: tdict[tkey] = telem.text # Handle duplicate table keys skey = script_elem.get('key') if skey in _elt_dict: if not isinstance(_elt_dict[skey], list): _elt_dict[skey] = [_elt_dict[skey], ] _elt_dict[skey].append(tdict) else: _elt_dict[skey] = tdict _script_dict['elements'] = _elt_dict return _script_dict @classmethod def __parse_host_scripts(cls, scripts_data): """ Private method parsing the data from scripts affecting the target host. Contents of is returned as a list of dict. :param scripts_data: portion of XML describing the results of the scripts data :type scripts_data: xml.ElementTree.Element or a string :return: python list holding scripts output in a dict """ _host_scripts = [] for xscript in scripts_data: if xscript.tag == 'script': _script_dict = cls.__parse_script(xscript) _host_scripts.append(_script_dict) return _host_scripts @classmethod def __parse_os_fingerprint(cls, os_data): """ Private method parsing the data from an OS fingerprint (-O). Contents of is returned as a dict. :param os_data: portion of XML describing the results of the os fingerprinting attempt :type os_data: xml.ElementTree.Element or a string :return: python dict representing the XML os tag """ rdict = {} xelement = cls.__format_element(os_data) os_class_probability = [] os_match_probability = [] os_ports_used = [] os_fingerprints = [] for xos in xelement: # for nmap xml version < 1.04, osclass is not # embedded in osmatch if xos.tag == 'osclass': os_class_proba = cls.__parse_osclass(xos) os_class_probability.append(os_class_proba) elif xos.tag == 'osmatch': os_match_proba = cls.__parse_osmatch(xos) os_match_probability.append(os_match_proba) elif xos.tag == 'portused': os_portused = cls.__format_attributes(xos) os_ports_used.append(os_portused) elif xos.tag == 'osfingerprint': os_fp_dict = cls.__format_attributes(xos) os_fingerprints.append(os_fp_dict) rdict['osmatches'] = os_match_probability rdict['osclasses'] = os_class_probability rdict['ports_used'] = os_ports_used rdict['osfingerprints'] = os_fingerprints return rdict @classmethod def __parse_osmatch(cls, osmatch_data): """ This methods parses osmatch data and returns a dict. Depending on the nmap xml version, osmatch could contain an osclass dict. :param osmatch_data: XML tag from a nmap scan :type osmatch_data: xml.ElementTree.Element or a string :return: python dict representing the XML osmatch tag """ rdict = {} xelement = cls.__format_element(osmatch_data) rdict['osmatch'] = cls.__format_attributes(xelement) rdict['osclasses'] = [] for xmltag in xelement: if xmltag.tag == 'osclass': _osclass_dict = cls.__parse_osclass(xmltag) rdict['osclasses'].append(_osclass_dict) else: exmsg = "Unexcepted node in : {0}".format(xmltag.tag) raise NmapParserException(exmsg) return rdict @classmethod def __parse_osclass(cls, osclass_data): """ This methods parses osclass data and returns a dict. Depending on the nmap xml version, osclass could contain a cpe dict. :param osclass_data: XML tag from a nmap scan :type osclass_data: xml.ElementTree.Element or a string :return: python dict representing the XML osclass tag """ rdict = {} xelement = cls.__format_element(osclass_data) rdict['osclass'] = cls.__format_attributes(xelement) rdict['cpe'] = [] for xmltag in xelement: if xmltag.tag == 'cpe': _cpe_string = xmltag.text rdict['cpe'].append(_cpe_string) else: exmsg = "Unexcepted node in : {0}".format(xmltag.tag) raise NmapParserException(exmsg) return rdict @classmethod def __parse_runstats(cls, scanrunstats_data): """ Private method parsing a portion of a nmap scan result. Receives a XML tag. :param scanrunstats_data: XML tag from a nmap scan :type scanrunstats_data: xml.ElementTree.Element or a string :return: python dict representing the XML runstats tag """ xelement = cls.__format_element(scanrunstats_data) rdict = {} for xmltag in xelement: if xmltag.tag in ['finished', 'hosts']: rdict[xmltag.tag] = cls.__format_attributes(xmltag) else: exmsg = "Unexcepted node in : {0}".format(xmltag.tag) raise NmapParserException(exmsg) return rdict @staticmethod def __format_element(elt_data): """ Private method which ensures that a XML portion to be parsed is of type xml.etree.ElementTree.Element. If elt_data is a string, then it is converted to an XML Element type. :param elt_data: XML Element to be parsed or string to be converted to a XML Element :return: Element """ if isinstance(elt_data, str): try: xelement = ET.fromstring(elt_data) except: raise NmapParserException("Error while trying " "to instanciate XML Element from " "string {0}".format(elt_data)) elif ET.iselement(elt_data): xelement = elt_data else: raise NmapParserException("Error while trying to parse supplied " "data: unsupported format") return xelement @staticmethod def __format_attributes(elt_data): """ Private method which converts a single XML tag to a python dict. It also checks that the elt_data given as argument is of type xml.etree.ElementTree.Element :param elt_data: XML Element to be parsed or string to be converted to a XML Element :return: Element """ rval = {} if not ET.iselement(elt_data): raise NmapParserException("Error while trying to parse supplied " "data attributes: format is not XML or " "XML tag is empty") try: for dkey in elt_data.keys(): rval[dkey] = elt_data.get(dkey) if rval[dkey] is None: raise NmapParserException("Error while trying to build-up " "element attributes: empty " "attribute {0}".format(dkey)) except: raise return rval class NmapParserException(Exception): def __init__(self, msg): self.msg = msg def __str__(self): return self.msg python-libnmap-0.7.0/libnmap/process.py0000664000175000017500000005232312664652334020733 0ustar ronaldronald00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import shlex import subprocess from threading import Thread from xml.dom import pulldom import warnings import platform try: import pwd except ImportError: pass __all__ = [ 'NmapProcess' ] class NmapTask(object): """ NmapTask is a internal class used by process. Each time nmap starts a new task during the scan, a new class will be instanciated. Classes examples are: "Ping Scan", "NSE script", "DNS Resolve",.. To each class an estimated time to complete is assigned and updated at least every second within the NmapProcess. A property NmapProcess.current_task points to the running task at time T and a dictionnary NmapProcess.tasks with "task name" as key is built during scan execution """ def __init__(self, name, starttime=0, extrainfo=''): self.name = name self.etc = 0 self.progress = 0 self.percent = 0 self.remaining = 0 self.status = 'started' self.starttime = starttime self.endtime = 0 self.extrainfo = extrainfo self.updated = 0 class NmapProcess(Thread): """ NmapProcess is a class which wraps around the nmap executable. Consequently, in order to run an NmapProcess, nmap should be installed on the host running the script. By default NmapProcess will produce the output of the nmap scan in the nmap XML format. This could be then parsed out via the NmapParser class from libnmap.parser module. """ def __init__(self, targets="127.0.0.1", options="-sT", event_callback=None, safe_mode=True, fqp=None): """ Constructor of NmapProcess class. :param targets: hosts to be scanned. Could be a string of hosts \ separated with a coma or a python list of hosts/ip. :type targets: string or list :param options: list of nmap options to be applied to scan. \ These options are all documented in nmap's man pages. :param event_callback: callable function which will be ran \ each time nmap process outputs data. This function will receive \ two parameters: 1. the nmap process object 2. the data produced by nmap process. See readme for examples. :param safe_mode: parameter to protect unsafe options like -oN, -oG, \ -iL, -oA,... :param fqp: full qualified path, if None, nmap will be searched \ in the PATH :return: NmapProcess object """ Thread.__init__(self) unsafe_opts = set(['-oG', '-oN', '-iL', '-oA', '-oS', '-oX', '--iflist', '--resume', '--stylesheet', '--datadir']) # more reliable than just using os.name() (cygwin) self.__is_windows = platform.system() == 'Windows' if fqp: if os.path.isfile(fqp) and os.access(fqp, os.X_OK): self.__nmap_binary = fqp else: raise EnvironmentError(1, "wrong path or not executable", fqp) else: nmap_binary_name = "nmap" self.__nmap_binary = self._whereis(nmap_binary_name) self.__nmap_fixed_options = "-oX - -vvv --stats-every 1s" if self.__nmap_binary is None: raise EnvironmentError(1, "nmap is not installed or could " "not be found in system path") if isinstance(targets, str): self.__nmap_targets = targets.replace(" ", "").split(',') elif isinstance(targets, list): self.__nmap_targets = targets else: raise Exception("Supplied target list should be either a " "string or a list") self._nmap_options = set(options.split()) if safe_mode and not self._nmap_options.isdisjoint(unsafe_opts): raise Exception("unsafe options activated while safe_mode " "is set True") self.__nmap_dynamic_options = options self.__sudo_run = '' self.__nmap_command_line = self.get_command_line() if event_callback and callable(event_callback): self.__nmap_event_callback = event_callback else: self.__nmap_event_callback = None (self.DONE, self.READY, self.RUNNING, self.CANCELLED, self.FAILED) = range(5) self._run_init() def _run_init(self): self.__nmap_command_line = self.get_command_line() # API usable in callback function self.__nmap_proc = None self.__nmap_rc = 0 self.__state = self.RUNNING self.__starttime = 0 self.__endtime = 0 self.__version = '' self.__elapsed = '' self.__summary = '' self.__stdout = '' self.__stderr = '' self.__current_task = '' self.__nmap_tasks = {} def _whereis(self, program): """ Protected method enabling the object to find the full path of a binary from its PATH environment variable. :param program: name of a binary for which the full path needs to be discovered. :return: the full path to the binary. :todo: add a default path list in case PATH is empty. """ split_char = ';' if self.__is_windows else ':' program = program + '.exe' if self.__is_windows else program for path in os.environ.get('PATH', '').split(split_char): if (os.path.exists(os.path.join(path, program)) and not os.path.isdir(os.path.join(path, program))): return os.path.join(path, program) return None def get_command_line(self): """ Public method returning the reconstructed command line ran via the lib :return: the full nmap command line to run :rtype: string """ return ("{0} {1} {2} {3} {4}".format(self.__sudo_run, self.__nmap_binary, self.__nmap_fixed_options, self.__nmap_dynamic_options, " ".join(self.__nmap_targets))) def sudo_run(self, run_as='root'): """ Public method enabling the library's user to run the scan with priviledges via sudo. The sudo configuration should be set manually on the local system otherwise sudo will prompt for a password. This method alters the command line by prefixing the sudo command to nmap and will then call self.run() :param run_as: user name to which the lib needs to sudo to run the scan :return: return code from nmap execution """ sudo_user = run_as.split().pop() try: pwd.getpwnam(sudo_user).pw_uid except KeyError: _exmsg = ("Username {0} does not exists. Please supply" " a valid username".format(run_as)) raise EnvironmentError(_exmsg) sudo_path = self._whereis("sudo") if sudo_path is None: raise EnvironmentError(2, "sudo is not installed or " "could not be found in system path: " "cannot run nmap with sudo") self.__sudo_run = "{0} -u {1}".format(sudo_path, sudo_user) rc = self.run() self.__sudo_run = "" return rc def sudo_run_background(self, run_as='root'): """ Public method enabling the library's user to run in background a nmap scan with priviledges via sudo. The sudo configuration should be set manually on the local system otherwise sudo will prompt for a password. This method alters the command line by prefixing the sudo command to nmap and will then call self.run() :param run_as: user name to which the lib needs to sudo to run the scan :return: return code from nmap execution """ sudo_user = run_as.split().pop() try: pwd.getpwnam(sudo_user).pw_uid except KeyError: _exmsg = ("Username {0} does not exists. Please supply" " a valid username".format(run_as)) raise EnvironmentError(_exmsg) sudo_path = self._whereis("sudo") if sudo_path is None: raise EnvironmentError(2, "sudo is not installed or " "could not be found in system path: " "cannot run nmap with sudo") self.__sudo_run = "{0} -u {1}".format(sudo_path, sudo_user) super(NmapProcess, self).start() def run(self): """ Public method which is usually called right after the constructor of NmapProcess. This method starts the nmap executable's subprocess. It will also bind a Process that will read from subprocess' stdout and stderr and push the lines read in a python queue for futher processing. This processing is waken-up each time data is pushed from the nmap binary into the stdout reading routine. Processing could be performed by a user-provided callback. The whole NmapProcess object could be accessible asynchroneously. return: return code from nmap execution """ self._run_init() _tmp_cmdline = self.__build_windows_cmdline() if self.__is_windows \ else shlex.split(self.__nmap_command_line) try: self.__nmap_proc = subprocess.Popen(args=_tmp_cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, bufsize=0) self.__state = self.RUNNING except OSError: self.__state = self.FAILED raise EnvironmentError(1, "nmap is not installed or could " "not be found in system path") while self.__nmap_proc.poll() is None: for streamline in iter(self.__nmap_proc.stdout.readline, ''): self.__stdout += streamline evnt = self.__process_event(streamline) if self.__nmap_event_callback and evnt: self.__nmap_event_callback(self) self.__stderr += self.__nmap_proc.stderr.read() self.__nmap_rc = self.__nmap_proc.poll() if self.rc is None: self.__state = self.CANCELLED elif self.rc == 0: self.__state = self.DONE if self.current_task: self.__nmap_tasks[self.current_task.name].progress = 100 else: self.__state = self.FAILED # Call the callback one last time to signal the new state if self.__nmap_event_callback: self.__nmap_event_callback(self) return self.rc def run_background(self): """ run nmap scan in background as a thread. For privileged scans, consider NmapProcess.sudo_run_background() """ self.__state = self.RUNNING super(NmapProcess, self).start() def is_running(self): """ Checks if nmap is still running. :return: True if nmap is still running """ return self.state == self.RUNNING def has_terminated(self): """ Checks if nmap has terminated. Could have failed or succeeded :return: True if nmap process is not running anymore. """ return (self.state == self.DONE or self.state == self.FAILED or self.state == self.CANCELLED) def has_failed(self): """ Checks if nmap has failed. :return: True if nmap process errored. """ return self.state == self.FAILED def is_successful(self): """ Checks if nmap terminated successfully. :return: True if nmap terminated successfully. """ return self.state == self.DONE def stop(self): """ Send KILL -15 to the nmap subprocess and gently ask the threads to stop. """ self.__state = self.CANCELLED if self.__nmap_proc.poll() is None: self.__nmap_proc.kill() def __process_event(self, eventdata): """ Private method called while nmap process is running. It enables the library to handle specific data/events produced by nmap process. So far, the following events are supported: 1. task progress: updates estimated time to completion and percentage done while scan is running. Could be used in combination with a callback function which could then handle this data while scan is running. 2. nmap run: header of the scan. Usually displayed when nmap is started 3. finished: when nmap scan ends. :return: True is event is known. :todo: handle parsing directly via NmapParser.parse() """ rval = False try: edomdoc = pulldom.parseString(eventdata) for xlmnt, xmlnode in edomdoc: if xlmnt is not None and xlmnt == pulldom.START_ELEMENT: if (xmlnode.nodeName == 'taskbegin' and xmlnode.attributes.keys()): xt = xmlnode.attributes taskname = xt['task'].value starttime = xt['time'].value xinfo = '' if 'extrainfo' in xt.keys(): xinfo = xt['extrainfo'].value newtask = NmapTask(taskname, starttime, xinfo) self.__nmap_tasks[newtask.name] = newtask self.__current_task = newtask.name rval = True elif (xmlnode.nodeName == 'taskend' and xmlnode.attributes.keys()): xt = xmlnode.attributes tname = xt['task'].value xinfo = '' self.__nmap_tasks[tname].endtime = xt['time'].value if 'extrainfo' in xt.keys(): xinfo = xt['extrainfo'].value self.__nmap_tasks[tname].extrainfo = xinfo self.__nmap_tasks[tname].status = "ended" rval = True elif (xmlnode.nodeName == 'taskprogress' and xmlnode.attributes.keys()): xt = xmlnode.attributes tname = xt['task'].value percent = xt['percent'].value etc = xt['etc'].value remaining = xt['remaining'].value updated = xt['time'].value self.__nmap_tasks[tname].percent = percent self.__nmap_tasks[tname].progress = percent self.__nmap_tasks[tname].etc = etc self.__nmap_tasks[tname].remaining = remaining self.__nmap_tasks[tname].updated = updated rval = True elif (xmlnode.nodeName == 'nmaprun' and xmlnode.attributes.keys()): self.__starttime = xmlnode.attributes['start'].value self.__version = xmlnode.attributes['version'].value rval = True elif (xmlnode.nodeName == 'finished' and xmlnode.attributes.keys()): self.__endtime = xmlnode.attributes['time'].value self.__elapsed = xmlnode.attributes['elapsed'].value self.__summary = xmlnode.attributes['summary'].value rval = True except: pass return rval def __build_windows_cmdline(self): cmdline = [] cmdline.append(self.__nmap_binary) if self.__nmap_fixed_options: cmdline += self.__nmap_fixed_options.split() if self.__nmap_dynamic_options: cmdline += self.__nmap_dynamic_options.split() if self.__nmap_targets: cmdline += self.__nmap_targets # already a list return cmdline @property def command(self): """ return the constructed nmap command or empty string if not constructed yet. :return: string """ return self.__nmap_command_line or '' @property def targets(self): """ Provides the list of targets to scan :return: list of string """ return self.__nmap_targets @property def options(self): """ Provides the list of options for that scan :return: list of string (nmap options) """ return self._nmap_options @property def state(self): """ Accessor for nmap execution state. Possible states are: - self.READY - self.RUNNING - self.FAILED - self.CANCELLED - self.DONE :return: integer (from above documented enum) """ return self.__state @property def starttime(self): """ Accessor for time when scan started :return: string. Unix timestamp """ return self.__starttime @property def endtime(self): """ Accessor for time when scan ended :return: string. Unix timestamp """ warnings.warn("data collected from finished events are deprecated." "Use NmapParser.parse()", DeprecationWarning) return self.__endtime @property def elapsed(self): """ Accessor returning for how long the scan ran (in seconds) :return: string """ warnings.warn("data collected from finished events are deprecated." "Use NmapParser.parse()", DeprecationWarning) return self.__elapsed @property def summary(self): """ Accessor returning a short summary of the scan's results :return: string """ warnings.warn("data collected from finished events are deprecated." "Use NmapParser.parse()", DeprecationWarning) return self.__summary @property def tasks(self): """ Accessor returning for the list of tasks ran during nmap scan :return: dict of NmapTask object """ return self.__nmap_tasks @property def version(self): """ Accessor for nmap binary version number :return: version number of nmap binary :rtype: string """ return self.__version @property def current_task(self): """ Accessor for the current NmapTask beeing run :return: NmapTask or None if no task started yet """ rval = None if len(self.__current_task): rval = self.tasks[self.__current_task] return rval @property def etc(self): """ Accessor for estimated time to completion :return: estimated time to completion """ rval = 0 if self.current_task: rval = self.current_task.etc return rval @property def progress(self): """ Accessor for progress status in percentage :return: percentage of job processed. """ rval = 0 if self.current_task: rval = self.current_task.progress return rval @property def rc(self): """ Accessor for nmap execution's return code :return: nmap execution's return code """ return self.__nmap_rc @property def stdout(self): """ Accessor for nmap standart output :return: output from nmap scan in XML :rtype: string """ return self.__stdout @property def stderr(self): """ Accessor for nmap standart error :return: output from nmap when errors occured. :rtype: string """ return self.__stderr def main(): def mycallback(nmapscan=None): if nmapscan.is_running() and nmapscan.current_task: ntask = nmapscan.current_task print("Task {0} ({1}): ETC: {2} DONE: {3}%".format(ntask.name, ntask.status, ntask.etc, ntask.progress)) nm = NmapProcess("scanme.nmap.org", options="-A", event_callback=mycallback) rc = nm.run() if rc == 0: print("Scan started at {0} nmap version: {1}").format(nm.starttime, nm.version) print("state: {0} (rc: {1})").format(nm.state, nm.rc) print("results size: {0}").format(len(nm.stdout)) print("Scan ended {0}: {1}").format(nm.endtime, nm.summary) else: print("state: {0} (rc: {1})").format(nm.state, nm.rc) print("Error: {stderr}").format(stderr=nm.stderr) print("Result: {0}").format(nm.stdout) if __name__ == '__main__': main() python-libnmap-0.7.0/libnmap/objects/0000775000175000017500000000000012664653634020333 5ustar ronaldronald00000000000000python-libnmap-0.7.0/libnmap/objects/os.py0000664000175000017500000003143012664652334021323 0ustar ronaldronald00000000000000# -*- coding: utf-8 -*- import warnings from libnmap.objects.cpe import CPE class OSFPPortUsed(object): """ Port used class: this enables the user of NmapOSFingerprint class to have a common and clear interface to access portused data which were collected and used during os fingerprint scan """ def __init__(self, port_used_dict): try: self._state = port_used_dict['state'] self._proto = port_used_dict['proto'] self._portid = port_used_dict['portid'] except KeyError: raise Exception("Cannot create OSFPPortUsed: missing required key") @property def state(self): """ Accessor for the portused state (closed, open,...) """ return self._state @property def proto(self): """ Accessor for the portused protocol (tcp, udp,...) """ return self._proto @property def portid(self): """ Accessor for the referenced port number used """ return self._portid class NmapOSMatch(object): """ NmapOSMatch is an internal class used for offering results from an nmap os fingerprint. This common interfaces makes a compatibility between old nmap xml (<1.04) and new nmap xml versions (used in nmapv6 for instance). In previous xml version, osclass tags from nmap fingerprints were not directly mapped to a osmatch. In new xml version, osclass could be embedded in osmatch tag. The approach to solve this is to create a common class which will, for older xml version, match based on the accuracy osclass to an osmatch. If no match, an osmatch will be made up from a concat of os class attributes: vendor and osfamily. Unmatched osclass will have a line attribute of -1. More info, see issue #26 or http://seclists.org/nmap-dev/2012/q2/252 """ def __init__(self, osmatch_dict): _osmatch_dict = osmatch_dict['osmatch'] if('name' not in _osmatch_dict or 'line' not in _osmatch_dict or 'accuracy' not in _osmatch_dict): raise Exception("Cannot create NmapOSClass: missing required key") self._name = _osmatch_dict['name'] self._line = _osmatch_dict['line'] self._accuracy = _osmatch_dict['accuracy'] # create osclass list self._osclasses = [] try: for _osclass in osmatch_dict['osclasses']: try: _osclassobj = NmapOSClass(_osclass) except: raise Exception("Could not create NmapOSClass object") self._osclasses.append(_osclassobj) except KeyError: pass def add_osclass(self, osclass_obj): """ Add a NmapOSClass object to the OSMatch object. This method is useful to implement compatibility with older versions of NMAP by providing a common interface to access os fingerprint data. """ self._osclasses.append(osclass_obj) @property def osclasses(self): """ Accessor for all NmapOSClass objects matching with this OS Match """ return self._osclasses @property def name(self): """ Accessor for name attribute (e.g.: Linux 2.4.26 (Slackware 10.0.0)) """ return self._name @property def line(self): """ Accessor for line attribute as integer. value equals -1 if this osmatch holds orphans NmapOSClass objects. This could happen with older version of nmap xml engine (<1.04 (e.g: nmapv6)). :return: int """ return int(self._line) @property def accuracy(self): """ Accessor for accuracy :return: int """ return int(self._accuracy) def get_cpe(self): """ This method return a list of cpe stings and not CPE objects as the NmapOSClass.cpelist property. This method is a helper to simplify data management. For more advanced handling of CPE data, use NmapOSClass.cpelist and use the methods from CPE class """ _cpelist = [] for osc in self.osclasses: for cpe in osc.cpelist: _cpelist.append(cpe.cpestring) return _cpelist def __repr__(self): rval = "{0}: {1}".format(self.name, self.accuracy) for _osclass in self._osclasses: rval += "\r\n |__ os class: {0}".format(str(_osclass)) return rval class NmapOSClass(object): """ NmapOSClass offers an unified API to access data from analysed osclass tag. As implemented in libnmap and newer version of nmap, osclass objects will always be embedded in a NmapOSMatch. Unmatched NmapOSClass will be stored in "dummy" NmapOSMatch objects which will have the particularity of have a line attribute of -1. On top of this, NmapOSClass will have optional CPE objects embedded. """ def __init__(self, osclass_dict): _osclass = osclass_dict['osclass'] if('vendor' not in _osclass or 'osfamily' not in _osclass or 'accuracy' not in _osclass): raise Exception("Wrong osclass structure: missing required key") self._vendor = _osclass['vendor'] self._osfamily = _osclass['osfamily'] self._accuracy = _osclass['accuracy'] self._osgen = '' self._type = '' # optional data if 'osgen' in _osclass: self._osgen = _osclass['osgen'] if 'type' in _osclass: self._type = _osclass['type'] self._cpelist = [] for _cpe in osclass_dict['cpe']: self._cpelist.append(CPE(_cpe)) @property def cpelist(self): """ Returns a list of CPE Objects matching with this os class :return: list of CPE objects :rtype: Array """ return self._cpelist @property def vendor(self): """ Accessor for vendor information (Microsoft, Linux,...) :return: string """ return self._vendor @property def osfamily(self): """ Accessor for OS family information (Windows, Linux,...) :return: string """ return self._osfamily @property def accuracy(self): """ Accessor for OS class detection accuracy (int) :return: int """ return int(self._accuracy) @property def osgen(self): """ Accessor for OS class generation (7, 8, 2.4.X,...). :return: string """ return self._osgen @property def type(self): """ Accessor for OS class type (general purpose,...) :return: string """ return self._type @property def description(self): """ Accessor helper which returns a concataned string of the valuable attributes from NmapOSClass object :return: string """ rval = "{0}: {1}, {2}".format(self.type, self.vendor, self.osfamily) if len(self.osgen): rval += "({0})".format(self.osgen) return rval def __repr__(self): rval = "{0}: {1}, {2}".format(self.type, self.vendor, self.osfamily) if len(self.osgen): rval += "({0})".format(self.osgen) for _cpe in self._cpelist: rval += "\r\n |__ {0}".format(str(_cpe)) return rval class NmapOSFingerprint(object): """ NmapOSFingerprint is a easier API for using os fingerprinting. Data for OS fingerprint ( tag) is instanciated from a NmapOSFingerprint which is accessible in NmapHost via NmapHost.os """ def __init__(self, osfp_data): self.__osmatches = [] self.__ports_used = [] self.__fingerprints = [] if 'osmatches' in osfp_data: for _osmatch in osfp_data['osmatches']: _osmatch_obj = NmapOSMatch(_osmatch) self.__osmatches.append(_osmatch_obj) if 'osclasses' in osfp_data: for _osclass in osfp_data['osclasses']: _osclass_obj = NmapOSClass(_osclass) _osmatched = self.get_osmatch(_osclass_obj) if _osmatched is not None: _osmatched.add_osclass(_osclass_obj) else: self._add_dummy_osmatch(_osclass_obj) if 'osfingerprints' in osfp_data: for _osfp in osfp_data['osfingerprints']: if 'fingerprint' in _osfp: self.__fingerprints.append(_osfp['fingerprint']) if 'ports_used' in osfp_data: for _pused_dict in osfp_data['ports_used']: _pused = OSFPPortUsed(_pused_dict) self.__ports_used.append(_pused) def get_osmatch(self, osclass_obj): """ This function enables NmapOSFingerprint to determine if an NmapOSClass object could be attached to an existing NmapOSMatch object in order to respect the common interface for the nmap xml version < 1.04 and >= 1.04 This method will return an NmapOSMatch object matching with the NmapOSClass provided in parameter (match is performed based on accuracy) :return: NmapOSMatch object """ rval = None for _osmatch in self.__osmatches: if _osmatch.accuracy == osclass_obj.accuracy: rval = _osmatch break # sorry return rval def _add_dummy_osmatch(self, osclass_obj): """ This functions creates a dummy NmapOSMatch object in order to encapsulate an NmapOSClass object which was not matched with an existing NmapOSMatch object """ _dname = "{0}:{1}:{2}".format(osclass_obj.type, osclass_obj.vendor, osclass_obj.osfamily) _dummy_dict = {'osmatch': {'name': _dname, 'accuracy': osclass_obj.accuracy, 'line': -1}, 'osclasses': []} _dummy_osmatch = NmapOSMatch(_dummy_dict) self.__osmatches.append(_dummy_osmatch) @property def osmatches(self, min_accuracy=0): _osmatches = [] for _osmatch in self.__osmatches: if _osmatch.accuracy >= min_accuracy: _osmatches.append(_osmatch) return _osmatches @property def osclasses(self, min_accuracy=0): osc_array = [] for _osm in self.osmatches: for _osc in _osm.osclasses: if _osc.accuracy >= min_accuracy: osc_array.append(_osc) return osc_array @property def fingerprint(self): return "\r\n".join(self.__fingerprints) @property def fingerprints(self): return self.__fingerprints @property def ports_used(self): """ Return an array of OSFPPortUsed object with the ports used to perform the os fingerprint. This dict might contain another dict embedded containing the ports_reason values. """ return self.__ports_used def osmatch(self, min_accuracy=90): warnings.warn("NmapOSFingerprint.osmatch is deprecated: " "use NmapOSFingerprint.osmatches", DeprecationWarning) os_array = [] for _osmatch in self.__osmatches: if _osmatch.accuracy >= min_accuracy: os_array.append(_osmatch.name) return os_array def osclass(self, min_accuracy=90): warnings.warn("NmapOSFingerprint.osclass() is deprecated: " "use NmapOSFingerprint.osclasses() if applicable", DeprecationWarning) os_array = [] for osmatch_entry in self.osmatches(): if osmatch_entry.accuracy >= min_accuracy: for oclass in osmatch_entry.osclasses: _ftstr = "type:{0}|vendor:{1}|osfamily{2}".format( oclass.type, oclass.vendor, oclass.osfamily) os_array.append(_ftstr) return os_array def os_cpelist(self): cpelist = [] for _osmatch in self.osmatches: for oclass in _osmatch.osclasses: cpelist.extend(oclass.cpelist) return cpelist def __repr__(self): rval = "" for _osmatch in self.osmatches: rval += "\r\n{0}".format(_osmatch) rval += "Fingerprints: ".format(self.fingerprint) return rval python-libnmap-0.7.0/libnmap/objects/__init__.py0000664000175000017500000000032612664652334022441 0ustar ronaldronald00000000000000# -*- coding: utf-8 -*- from libnmap.objects.report import NmapReport from libnmap.objects.host import NmapHost from libnmap.objects.service import NmapService __all__ = ['NmapReport', 'NmapHost', 'NmapService'] python-libnmap-0.7.0/libnmap/objects/service.py0000664000175000017500000002157312664652334022351 0ustar ronaldronald00000000000000# -*- coding: utf-8 -*- from libnmap.diff import NmapDiff from libnmap.objects.os import CPE class NmapService(object): """ NmapService represents a nmap scanned service. Its id() is comprised of the protocol and the port. Depending on the scanning options, some additional details might be available or not. Like banner or extra datas from NSE (nmap scripts). """ def __init__(self, portid, protocol='tcp', state=None, service=None, owner=None, service_extras=None): """ Constructor :param portid: port number :type portid: string :param protocol: protocol of port scanned (tcp, udp) :type protocol: string :param state: python dict describing the service status :type state: python dict :param service: python dict describing the service name and banner :type service: python dict :param service_extras: additional info about the tested service like scripts' data """ try: self._portid = int(portid or -1) except (ValueError, TypeError): raise if self._portid < 0 or self._portid > 65535: raise ValueError self._protocol = protocol self._state = state if state is not None else {} self._service = service if service is not None else {} self._cpelist = [] if 'cpelist' in self._service: for _cpe in self._service['cpelist']: _cpeobj = CPE(_cpe) self._cpelist.append(_cpeobj) self._owner = '' if owner is not None and 'name' in owner: self._owner = owner['name'] self._reason = '' self._reason_ip = '' self._reason_ttl = '' self._servicefp = '' self._tunnel = '' if 'reason' in self._state: self._reason = self._state['reason'] if 'reason_ttl' in self._state: self._reason_ttl = self._state['reason_ttl'] if 'reason_ip' in self._state: self._reason_ip = self._state['reason_ip'] if 'servicefp' in self._service: self._servicefp = self._service['servicefp'] if 'tunnel' in self._service: self._tunnel = self._service['tunnel'] self._service_extras = [] if service_extras is not None: self._service_extras = service_extras def __eq__(self, other): """ Compares two NmapService objects to see if they are the same or if one of them changed. :param other: NmapService :return: boolean """ rval = False if(self.__class__ == other.__class__ and self.id == other.id): rval = (self.changed(other) == 0) return rval def __ne__(self, other): """ Compares two NmapService objects to see if they are different if one of them changed. :param other: NmapService :return: boolean """ rval = True if(self.__class__ == other.__class__ and self.id == other.id): rval = (self.changed(other) > 0) return rval def __repr__(self): return "{0}: [{1} {2}/{3} {4} ({5})]".format(self.__class__.__name__, self.state, str(self.port), self.protocol, self.service, self.banner) def __hash__(self): return (hash(self.port) ^ hash(self.protocol) ^ hash(self.state) ^ hash(self.reason) ^ hash(self.service) ^ hash(self.banner)) def changed(self, other): """ Checks if a NmapService is different from another. :param other: NmapService :return: boolean """ return len(self.diff(other).changed()) @property def port(self): """ Accessor for port. :return: integer or -1 """ return self._portid @property def protocol(self): """ Accessor for protocol :return: string """ return self._protocol @property def state(self): """ Accessor for service's state (open, filtered, closed,...) :return: string """ return self._state['state'] if 'state' in self._state else None @property def reason(self): """ Accessor for service's state reason (syn-ack, filtered,...) :return: string or empty if not applicable """ return self._reason @property def reason_ip(self): """ Accessor for service's state reason ip :return: string or empty if not applicable """ return self._reason_ip @property def reason_ttl(self): """ Accessor for service's state reason ttl :return: string or empty if not applicable """ return self._reason_ttl @property def service(self): """ Accessor for service name. :return: string or empty """ return self._service['name'] if 'name' in self._service else '' @property def service_dict(self): """ Accessor for service dictionary. :return: dict or None """ return self._service def open(self): """ Tells if the port was open or not :return: boolean """ return 'state' in self._state and self._state['state'] == 'open' @property def owner(self): """ Accessor for service owner if available """ return self._owner @property def banner(self): """ Accessor for the service's banner. Only available if the nmap option -sV or similar was used. :return: string """ notrelevant = ['name', 'method', 'conf', 'cpelist', 'servicefp', 'tunnel'] relevant = ['product', 'version', 'extrainfo'] b = '' skeys = self._service.keys() if 'method' in self._service and self._service['method'] == "probed": for relk in relevant: if relk in skeys: b += '{0}: {1} '.format(relk, self._service[relk]) for mkey in skeys: if mkey not in notrelevant and mkey not in relevant: b += '{0}: {1} '.format(mkey, self._service[mkey]) return b.rstrip() @property def cpelist(self): """ Accessor for list of CPE for this particular service """ return self._cpelist @property def scripts_results(self): """ Gives a python list of the nse scripts results. The dict key is the name (id) of the nse script and the value is the output of the script. :return: dict """ scripts_dict = None try: scripts_dict = self._service_extras['scripts'] except (KeyError, TypeError): pass return scripts_dict @property def servicefp(self): """ Accessor for the service's fingerprint if the nmap option -sV or -A is used :return: string if available """ return self._servicefp @property def tunnel(self): """ Accessor for the service's tunnel type if applicable and available from scan results :return: string if available """ return self._tunnel @property def id(self): """ Accessor for the id() of the NmapService. This is used for diff()ing NmapService object via NmapDiff. :return: tuple """ return "{0}.{1}".format(self.protocol, self.port) def get_dict(self): """ Return a python dict representation of the NmapService object. This is used to diff() NmapService objects via NmapDiff. :return: dict """ return ({'id': str(self.id), 'port': str(self.port), 'protocol': self.protocol, 'banner': self.banner, 'service': self.service, 'state': self.state, 'reason': self.reason}) def diff(self, other): """ Calls NmapDiff to check the difference between self and another NmapService object. Will return a NmapDiff object. This objects return python set() of keys describing the elements which have changed, were added, removed or kept unchanged. :return: NmapDiff object """ return NmapDiff(self, other) python-libnmap-0.7.0/libnmap/objects/report.py0000664000175000017500000002405412664652334022221 0ustar ronaldronald00000000000000# -*- coding: utf-8 -*- from libnmap.diff import NmapDiff class NmapReport(object): """ NmapReport is the usual interface for the end user to read scans output. A NmapReport as the following structure: - Scan headers data - A list of scanned hosts (NmapReport.hosts) - Scan footer data It is to note that each NmapHost comprised in NmapReport.hosts array contains also a list of scanned services (NmapService object). This means that if NmapParser.parse*() is the input interface for the end user of the lib. NmapReport is certainly the output interface for the end user of the lib. """ def __init__(self, raw_data=None): """ Constructor for NmapReport object. This is usually called by the NmapParser module. """ self._nmaprun = {} self._scaninfo = {} self._hosts = [] self._runstats = {} if raw_data is not None: self.__set_raw_data(raw_data) def save(self, backend): """ This method gets a NmapBackendPlugin representing the backend. :param backend: libnmap.plugins.PluginBackend object. Object created by BackendPluginFactory and enabling nmap reports to be saved/stored in any type of backend implemented in plugins. The primary key of the stored object is returned. :return: str """ if backend is not None: _id = backend.insert(self) else: raise RuntimeError return _id def diff(self, other): """ Calls NmapDiff to check the difference between self and another NmapReport object. Will return a NmapDiff object. :return: NmapDiff object :todo: remove is_consistent approach, diff() should be generic. """ if self.is_consistent() and other.is_consistent(): _rdiff = NmapDiff(self, other) else: _rdiff = set() return _rdiff @property def started(self): """ Accessor returning a unix timestamp of when the scan was started. :return: integer """ rval = -1 try: s_start = self._nmaprun['start'] rval = int(s_start) except(KeyError, TypeError, ValueError): pass return rval @property def commandline(self): """ Accessor returning the full nmap command line fired. :return: string """ return self._nmaprun['args'] @property def version(self): """ Accessor returning the version of the nmap binary used to perform the scan. :return: string """ return self._nmaprun['version'] @property def scan_type(self): """ Accessor returning a string which identifies what type of scan was launched (syn, ack, tcp,...). :return: string """ return self._scaninfo['type'] @property def hosts(self): """ Accessor returning an array of scanned hosts. Scanned hosts are NmapHost objects. :return: array of NmapHost """ return self._hosts def get_host_byid(self, host_id): """ Gets a NmapHost object directly from the host array by looking it up by id. :param ip_addr: ip address of the host to lookup :type ip_addr: string :return: NmapHost """ rval = None for _rhost in self._hosts: if _rhost.address == host_id: rval = _rhost return rval @property def endtime(self): """ Accessor returning a unix timestamp of when the scan ended. :return: integer """ rval = -1 try: rval = int(self._runstats['finished']['time']) except(KeyError, TypeError, ValueError): pass return rval @property def endtimestr(self): """ Accessor returning a human readable time string of when the scan ended. :return: string """ rval = '' try: rval = self._runstats['finished']['timestr'] except(KeyError, TypeError, ValueError): pass return rval @property def summary(self): """ Accessor returning a string describing and summarizing the scan. :return: string """ rval = '' try: rval = self._runstats['finished']['summary'] except(KeyError, TypeError): pass if len(rval) == 0: rval = ("Nmap ended at {0} ; {1} IP addresses ({2} hosts up)" " scanned in {3} seconds".format(self.endtimestr, self.hosts_total, self.hosts_up, self.elapsed)) return rval @property def elapsed(self): """ Accessor returning the number of seconds the scan took :return: float (0 >= or -1) """ rval = -1 try: s_elapsed = self._runstats['finished']['elapsed'] rval = float(s_elapsed) except (KeyError, TypeError, ValueError): rval = -1 return rval @property def hosts_up(self): """ Accessor returning the numer of host detected as 'up' during the scan. :return: integer (0 >= or -1) """ rval = -1 try: s_up = self._runstats['hosts']['up'] rval = int(s_up) except (KeyError, TypeError, ValueError): rval = -1 return rval @property def hosts_down(self): """ Accessor returning the numer of host detected as 'down' during the scan. :return: integer (0 >= or -1) """ rval = -1 try: s_down = self._runstats['hosts']['down'] rval = int(s_down) except (KeyError, TypeError, ValueError): rval = -1 return rval @property def hosts_total(self): """ Accessor returning the number of hosts scanned in total. :return: integer (0 >= or -1) """ rval = -1 try: s_total = self._runstats['hosts']['total'] rval = int(s_total) except (KeyError, TypeError, ValueError): rval = -1 return rval def get_raw_data(self): """ Returns a dict representing the NmapReport object. :return: dict :todo: deprecate. get rid of this uglyness. """ raw_data = {'_nmaprun': self._nmaprun, '_scaninfo': self._scaninfo, '_hosts': self._hosts, '_runstats': self._runstats} return raw_data def __set_raw_data(self, raw_data): self._nmaprun = raw_data['_nmaprun'] self._scaninfo = raw_data['_scaninfo'] self._hosts = raw_data['_hosts'] self._runstats = raw_data['_runstats'] def is_consistent(self): """ Checks if the report is consistent and can be diffed(). This needs to be rewritten and removed: diff() should be generic. :return: boolean """ rval = False rdata = self.get_raw_data() _consistent_keys = ['_nmaprun', '_scaninfo', '_hosts', '_runstats'] if(set(_consistent_keys) == set(rdata.keys()) and len([dky for dky in rdata.keys() if rdata[dky] is not None]) == 4): rval = True return rval def get_dict(self): """ Return a python dict representation of the NmapReport object. This is used to diff() NmapReport objects via NmapDiff. :return: dict """ rdict = dict([("{0}::{1}".format(_host.__class__.__name__, str(_host.id)), hash(_host)) for _host in self.hosts]) rdict.update({'commandline': self.commandline, 'version': self.version, 'scan_type': self.scan_type, 'elapsed': self.elapsed, 'hosts_up': self.hosts_up, 'hosts_down': self.hosts_down, 'hosts_total': self.hosts_total}) return rdict @property def id(self): """ Dummy id() defined for reports. """ return hash(1) def __eq__(self, other): """ Compare eq NmapReport based on : - create a diff obj and check the result report are equal if added&changed&removed are empty :return: boolean """ rval = False if(self.__class__ == other.__class__ and self.id == other.id): diffobj = self.diff(other) rval = (len(diffobj.changed()) == 0 and len(diffobj.added()) == 0 and len(diffobj.removed()) == 0 ) return rval def __ne__(self, other): """ Compare ne NmapReport based on: - create a diff obj and check the result report are ne if added|changed|removed are not empty :return: boolean """ rval = True if(self.__class__ == other.__class__ and self.id == other.id): diffobj = self.diff(other) rval = (len(diffobj.changed()) != 0 or len(diffobj.added()) != 0 or len(diffobj.removed()) != 0 ) return rval def __repr__(self): return "{0}: started at {1} hosts up {2}/{3}".format( self.__class__.__name__, self.started, self.hosts_up, self.hosts_total) python-libnmap-0.7.0/libnmap/objects/host.py0000664000175000017500000003352112664652334021662 0ustar ronaldronald00000000000000# -*- coding: utf-8 -*- from libnmap.diff import NmapDiff from libnmap.objects.os import NmapOSFingerprint class NmapHost(object): """ NmapHost is a class representing a host object of NmapReport """ def __init__(self, starttime='', endtime='', address=None, status=None, hostnames=None, services=None, extras=None): """ NmapHost constructor :param starttime: unix timestamp of when the scan against that host started :type starttime: string :param endtime: unix timestamp of when the scan against that host ended :type endtime: string :param address: dict ie :{'addr': '127.0.0.1', 'addrtype': 'ipv4'} :param status: dict ie:{'reason': 'localhost-response', 'state': 'up'} :return: NmapHost: """ self._starttime = starttime self._endtime = endtime self._hostnames = hostnames if hostnames is not None else [] self._status = status if status is not None else {} self._services = services if services is not None else [] self._extras = extras if extras is not None else {} self._osfingerprinted = False self.os = None if 'os' in self._extras: self.os = NmapOSFingerprint(self._extras['os']) self._osfingerprinted = True else: self.os = NmapOSFingerprint({}) self._ipv4_addr = None self._ipv6_addr = None self._mac_addr = None self._vendor = None for addr in address: if addr['addrtype'] == "ipv4": self._ipv4_addr = addr['addr'] elif addr['addrtype'] == 'ipv6': self._ipv6_addr = addr['addr'] elif addr['addrtype'] == 'mac': self._mac_addr = addr['addr'] if 'vendor' in addr: self._vendor = addr['vendor'] self._main_address = self._ipv4_addr or self._ipv6_addr or '' self._address = address def __eq__(self, other): """ Compare eq NmapHost based on : - hostnames - address - if an associated services has changed :return: boolean """ rval = False if(self.__class__ == other.__class__ and self.id == other.id): rval = (self.changed(other) == 0) return rval def __ne__(self, other): """ Compare ne NmapHost based on: - hostnames - address - if an associated services has changed :return: boolean """ rval = True if(self.__class__ == other.__class__ and self.id == other.id): rval = (self.changed(other) > 0) return rval def __repr__(self): """ String representing the object :return: string """ return "{0}: [{1} ({2}) - {3}]".format(self.__class__.__name__, self.address, " ".join(self._hostnames), self.status) def __hash__(self): """ Hash is needed to be able to use our object in sets :return: hash """ return (hash(self.status) ^ hash(self.address) ^ hash(frozenset(self._services)) ^ hash(frozenset(" ".join(self._hostnames)))) def changed(self, other): """ return the number of attribute who have changed :param other: NmapHost object to compare :return int """ return len(self.diff(other).changed()) @property def starttime(self): """ Accessor for the unix timestamp of when the scan was started :return: string """ return self._starttime @property def endtime(self): """ Accessor for the unix timestamp of when the scan ended :return: string """ return self._endtime @property def address(self): """ Accessor for the IP address of the scanned host :return: IP address as a string """ return self._main_address @address.setter def address(self, addrdict): """ Setter for the address dictionnary. :param addrdict: valid dict is {'addr': '1.1.1.1', 'addrtype': 'ipv4'} """ if addrdict['addrtype'] == 'ipv4': self._ipv4_addr = addrdict['addr'] elif addrdict['addrtype'] == 'ipv6': self._ipv6_addr = addrdict['addr'] elif addrdict['addrtype'] == 'mac': self._mac_addr = addrdict['addr'] if 'vendor' in addrdict: self._vendor = addrdict['vendor'] self._main_address = self._ipv4_addr or self._ipv6_addr or '' self._address = addrdict @property def ipv4(self): """ Accessor for the IPv4 address of the scanned host :return: IPv4 address as a string """ return self._ipv4_addr or '' @property def mac(self): """ Accessor for the MAC address of the scanned host :return: MAC address as a string """ return self._mac_addr or '' @property def vendor(self): """ Accessor for the vendor attribute of the scanned host :return: string (vendor) of empty string if no vendor defined """ return self._vendor or '' @property def ipv6(self): """ Accessor for the IPv6 address of the scanned host :return: IPv6 address as a string """ return self._ipv6_addr or '' @property def status(self): """ Accessor for the host's status (up, down, unknown...) :return: string """ return self._status['state'] @status.setter def status(self, statusdict): """ Setter for the status dictionnary. :param statusdict: valid dict is {"state": "open", "reason": "syn-ack", "reason_ttl": "0"} 'state' is the only mandatory key. """ self._status = statusdict def is_up(self): """ method to determine if host is up or not :return: bool """ rval = False if self.status == 'up': rval = True return rval @property def hostnames(self): """ Accessor returning the list of hostnames (array of strings). :return: array of string """ return self._hostnames @property def services(self): """ Accessor for the array of scanned services for that host. An array of NmapService objects is returned. :return: array of NmapService """ return self._services def get_ports(self): """ Retrieve a list of the port used by each service of the NmapHost :return: list: of tuples (port,'proto') ie:[(22,'tcp'),(25, 'tcp')] """ return [(p.port, p.protocol) for p in self._services] def get_open_ports(self): """ Same as get_ports() but only for open ports :return: list: of tuples (port,'proto') ie:[(22,'tcp'),(25, 'tcp')] """ return ([(p.port, p.protocol) for p in self._services if p.state == 'open']) def get_service(self, portno, protocol='tcp'): """ :param portno: int the portnumber :param protocol='tcp': string ('tcp','udp') :return: NmapService or None """ plist = [p for p in self._services if p.port == portno and p.protocol == protocol] if len(plist) > 1: raise Exception("Duplicate services found in NmapHost object") return plist.pop() if len(plist) else None def get_service_byid(self, service_id): """ Returns a NmapService by providing its id. The id of a nmap service is a python tupl made of (protocol, port) """ rval = None for _tmpservice in self._services: if _tmpservice.id == service_id: rval = _tmpservice return rval def os_class_probabilities(self): """ Returns an array of possible OS class detected during the OS fingerprinting. :return: Array of NmapOSClass objects """ rval = [] if self.os is not None: rval = self.os.osclasses return rval def os_match_probabilities(self): """ Returns an array of possible OS match detected during the OS fingerprinting :return: array of NmapOSMatches objects """ rval = [] if self.os is not None: rval = self.os.osmatches return rval @property def os_fingerprinted(self): """ Specify if the host has OS fingerprint data available :return: Boolean """ return self._osfingerprinted @property def os_fingerprint(self): """ Returns the fingerprint of the scanned system. :return: string """ rval = '' if self.os is not None: rval = "\n".join(self.os.fingerprints) return rval def os_ports_used(self): """ Returns an array of the ports used for OS fingerprinting :return: array of ports used: [{'portid': '22', 'proto': 'tcp', 'state': 'open'},] """ rval = [] try: rval = self._extras['os']['ports_used'] except (KeyError, TypeError): pass return rval @property def tcpsequence(self): """ Returns the difficulty to determine remotely predict the tcp sequencing. return: string """ rval = '' try: rval = self._extras['tcpsequence']['difficulty'] except (KeyError, TypeError): pass return rval @property def ipsequence(self): """ Return the class of ip sequence of the remote hosts. :return: string """ rval = '' try: rval = self._extras['ipidsequence']['class'] except (KeyError, TypeError): pass return rval @property def uptime(self): """ uptime of the remote host (if nmap was able to determine it) :return: string (in seconds) """ rval = 0 try: rval = int(self._extras['uptime']['seconds']) except (KeyError, TypeError): pass return rval @property def lastboot(self): """ Since when the host was booted. :return: string """ rval = '' try: rval = self._extras['uptime']['lastboot'] except (KeyError, TypeError): pass return rval @property def distance(self): """ Number of hops to host :return: int """ rval = 0 try: rval = int(self._extras['distance']['value']) except (KeyError, TypeError): pass return rval @property def scripts_results(self): """ Scripts results specific to the scanned host :return: array of