#!/usr/bin/env python 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'): """ 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"). 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) else: raise NmapParserException("Unknown data type provided. " "Please check documentation for " "supported data types.") return nmapobj @classmethod def _parse_xml(cls, nmap_data=None): """ 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 :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") 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"): """ 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. :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) @classmethod def parse_fromfile(cls, nmap_report_path, data_type="XML"): """ 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. :return: NmapObject """ try: with open(nmap_report_path, 'r') as fileobj: fdata = fileobj.read() rval = cls.parse(fdata, data_type) 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 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'], 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'], 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 = {} _address = {} _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': for port in cls._parse_xml_ports(xh): _services.append(port) elif xh.tag == 'status': _status = cls.__format_attributes(xh) elif xh.tag == 'address': _address = cls.__format_attributes(xh) elif xh.tag == 'os': _os_extra = cls.__parse_os_fingerprint(xh) _host_extras.update({'os': _os_extra}) 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, _address, _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) ports = [] for xservice in xelement: if xservice.tag == 'port': nport = cls._parse_xml_port(xservice) ports.append(nport) #else: # print "struct port unknown attr: %s value: %s" % # (h.tag, h.get(h.tag)) return ports @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 _service_extras = [] for xport in xelement: if xport.tag == 'state': _state = cls.__format_attributes(xport) elif xport.tag == 'service': _service = cls.__format_attributes(xport) elif xport.tag == 'script': _script_dict = cls.__format_attributes(xport) _service_extras.append(_script_dict) if(_portid is None or _protocol is None or _state is None or _service is None): raise NmapParserException("XML tag is incomplete. One " "of the following tags is missing: " "portid, protocol state or service.") nport = NmapService(_portid, _protocol, _state, _service, _service_extras) return nport @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_fp = {} for xos in xelement: if xos.tag == 'osclass': os_class_proba = cls.__format_attributes(xos) os_class_probability.append(os_class_proba) elif xos.tag == 'osmatch': os_match_proba = cls.__format_attributes(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 = cls.__format_attributes(xos) rdict['osmatch'] = os_match_probability rdict['osclass'] = os_class_probability rdict['ports_used'] = os_ports_used rdict['osfingerprint'] = os_fp['fingerprint'] 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: raise NmapParserException("Unpexpected data structure " "for ") 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