• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

savon-noir / python-libnmap / 13000574708

27 Jan 2025 11:47PM UTC coverage: 72.735% (+0.03%) from 72.708%
13000574708

push

github

savon-noir
fix: simplified the extraports structs

13 of 13 new or added lines in 3 files covered. (100.0%)

40 existing lines in 2 files now uncovered.

1750 of 2406 relevant lines covered (72.73%)

2.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.69
/libnmap/parser.py
1
# -*- coding: utf-8 -*-
2

3

4
try:
3✔
5
    import defusedxml.ElementTree as ET
3✔
6
except ImportError:
×
7
    try:
×
8
        import xml.etree.cElementTree as ET
×
9
    except ImportError:
×
10
        import xml.etree.ElementTree as ET
×
11

12
from xml.etree.ElementTree import iselement as et_iselement
3✔
13

14
from libnmap.objects import NmapHost, NmapReport, NmapService
3✔
15

16

17
class NmapParser(object):
3✔
18
    @classmethod
3✔
19
    def parse(cls, nmap_data=None, data_type="XML", incomplete=False):
3✔
20
        """
21
        Generic class method of NmapParser class.
22

23
        The data to be parsed does not need to be a complete nmap
24
        scan report. You can possibly give <hosts>...</hosts>
25
        or <port> XML tags.
26

27
        :param nmap_data: any portion of nmap scan result. \
28
        nmap_data should always be a string representing a part \
29
        or a complete nmap scan report.
30
        :type nmap_data: string
31

32
        :param data_type: specifies the type of data to be parsed.
33
        :type data_type: string ("XML"|"JSON"|"YAML").
34

35
        :param incomplete: enable you to parse interrupted nmap scans \
36
        and/or incomplete nmap xml blocks by adding a </nmaprun> at \
37
        the end of the scan. Be aware that this flag does not work for \
38
        already valid XML files, because adding an XML tag will \
39
        invalidate the XML.
40
        :type incomplete: boolean
41

42
        As of today, only XML parsing is supported.
43

44
        :return: NmapObject (NmapHost, NmapService or NmapReport)
45
        """
46

47
        nmapobj = None
3✔
48
        if data_type == "XML":
3✔
49
            nmapobj = cls._parse_xml(nmap_data, incomplete)
3✔
50
        else:
51
            raise NmapParserException(
3✔
52
                "Unknown data type provided. "
53
                "Please check documentation for "
54
                "supported data types."
55
            )
56
        return nmapobj
3✔
57

58
    @classmethod
3✔
59
    def _parse_xml(cls, nmap_data=None, incomplete=False):
3✔
60
        """
61
        Protected class method used to process a specific data type.
62
        In this case: XML. This method is called by cls.parse class
63
        method and receives nmap scan results data (in XML).
64

65
        :param nmap_data: any portion of nmap scan result can be given \
66
        as argument. nmap_data should always be a string representing \
67
        a part or a complete nmap scan report.
68
        :type nmap_data: string
69

70
        This method checks which portion of a nmap scan is given \
71
        as argument.
72
        It could be:
73

74
            1. a full nmap scan report;
75
            2. a scanned host: <host> tag in a nmap scan report
76
            3. a scanned service: <port> tag
77
            4. a list of hosts: <hosts/> tag (TODO)
78
            5. a list of ports: <ports/> tag
79

80
        :param incomplete: enable you to parse interrupted nmap scans \
81
        and/or incomplete nmap xml blocks by adding a </nmaprun> at \
82
        the end of the scan.
83
        :type incomplete: boolean
84

85
        :return: NmapObject (NmapHost, NmapService or NmapReport) \
86
                or a list of NmapObject
87
        """
88

89
        if not nmap_data:
3✔
90
            raise NmapParserException(
3✔
91
                "No report data to parse: please "
92
                "provide a valid XML nmap report"
93
            )
94
        elif not isinstance(nmap_data, str):
3✔
95
            raise NmapParserException(
3✔
96
                "wrong nmap_data type given as argument: cannot parse data"
97
            )
98

99
        if incomplete is True:
3✔
100
            nmap_data += "</nmaprun>"
×
101

102
        try:
3✔
103
            root = ET.fromstring(nmap_data)
3✔
104
        except Exception as e:
3✔
105
            emsg = "Wrong XML structure: cannot parse data: {0}".format(e)
3✔
106
            raise NmapParserException(emsg)
3✔
107

108
        nmapobj = None
3✔
109
        if root.tag == "nmaprun":
3✔
110
            nmapobj = cls._parse_xml_report(root)
3✔
111
        elif root.tag == "host":
3✔
112
            nmapobj = cls._parse_xml_host(root)
3✔
113
        elif root.tag == "ports":
3✔
114
            nmapobj = cls._parse_xml_ports(root)
3✔
115
        elif root.tag == "port":
3✔
116
            nmapobj = cls._parse_xml_port(root)
3✔
117
        else:
118
            raise NmapParserException(
×
119
                "Unpexpected data structure for XML " "root node"
120
            )
121
        return nmapobj
3✔
122

123
    @classmethod
3✔
124
    def _parse_xml_report(cls, root=None):
3✔
125
        """
126
        This method parses out a full nmap scan report from its XML root
127
        node: <nmaprun>.
128

129
        :param root: Element from xml.ElementTree (top of XML the document)
130
        :type root: Element
131

132
        :return: NmapReport object
133
        """
134

135
        nmap_scan = {
3✔
136
            "_nmaprun": {},
137
            "_scaninfo": {},
138
            "_hosts": [],
139
            "_runstats": {},
140
        }
141

142
        if root is None:
3✔
143
            raise NmapParserException(
×
144
                "No root node provided to parse XML report"
145
            )
146

147
        nmap_scan["_nmaprun"] = cls.__format_attributes(root)
3✔
148
        for el in root:
3✔
149
            if el.tag == "scaninfo":
3✔
150
                nmap_scan["_scaninfo"] = cls.__parse_scaninfo(el)
3✔
151
            elif el.tag == "host":
3✔
152
                nmap_scan["_hosts"].append(cls._parse_xml_host(el))
3✔
153
            elif el.tag == "runstats":
3✔
154
                nmap_scan["_runstats"] = cls.__parse_runstats(el)
3✔
155
            # else:
156
            #    print "struct pparse unknown attr: {0} value: {1}".format(
157
            #        el.tag,
158
            #        el.get(el.tag))
159
        return NmapReport(nmap_scan)
3✔
160

161
    @classmethod
3✔
162
    def parse_fromstring(cls, nmap_data, data_type="XML", incomplete=False):
3✔
163
        """
164
        Call generic cls.parse() method and ensure that a string is \
165
        passed on as argument. If not, an exception is raised.
166

167
        :param nmap_data: Same as for parse(), any portion of nmap scan. \
168
        Reports could be passed as argument. Data type _must_ be a string.
169

170
        :type nmap_data: string
171

172
        :param data_type: Specifies the type of data passed on as argument.
173

174
        :param incomplete: enable you to parse interrupted nmap scans \
175
        and/or incomplete nmap xml blocks by adding a </nmaprun> at \
176
        the end of the scan.
177
        :type incomplete: boolean
178

179
        :return: NmapObject
180
        """
181

182
        if not isinstance(nmap_data, str):
3✔
183
            raise NmapParserException(
×
184
                "bad argument type for "
185
                "parse_fromstring(): should be a string"
186
            )
187
        return cls.parse(nmap_data, data_type, incomplete)
3✔
188

189
    @classmethod
3✔
190
    def parse_fromfile(
3✔
191
        cls, nmap_report_path, data_type="XML", incomplete=False
192
    ):
193
        """
194
        Call generic cls.parse() method and ensure that a correct file \
195
        path is given as argument. If not, an exception is raised.
196

197
        :param nmap_data: Same as for parse(). \
198
        Any portion of nmap scan reports could be passed as argument. \
199
        Data type _must be a valid path to a file containing \
200
        nmap scan results.
201

202
        :param data_type: Specifies the type of serialization in the file.
203

204
        :param incomplete: enable you to parse interrupted nmap scans \
205
        and/or incomplete nmap xml blocks by adding a </nmaprun> at \
206
        the end of the scan.
207
        :type incomplete: boolean
208

209
        :return: NmapObject
210
        """
211

212
        try:
3✔
213
            with open(nmap_report_path, "r") as fileobj:
3✔
214
                fdata = fileobj.read()
3✔
215
                rval = cls.parse(fdata, data_type, incomplete)
3✔
216
        except IOError:
3✔
217
            raise
×
218
        return rval
3✔
219

220
    @classmethod
3✔
221
    def parse_fromdict(cls, rdict):
3✔
222
        """
223
        Strange method which transforms a python dict \
224
        representation of a NmapReport and turns it into an \
225
        NmapReport object. \
226
        Needs to be reviewed and possibly removed.
227

228
        :param rdict: python dict representation of an NmapReport
229
        :type rdict: dict
230

231
        :return: NmapReport
232
        """
233

234
        nreport = {}
3✔
235
        if list(rdict.keys())[0] == "__NmapReport__":
3✔
236
            r = rdict["__NmapReport__"]
3✔
237
            nreport["_runstats"] = r["_runstats"]
3✔
238
            nreport["_scaninfo"] = r["_scaninfo"]
3✔
239
            nreport["_nmaprun"] = r["_nmaprun"]
3✔
240
            hlist = []
3✔
241
            for h in r["_hosts"]:
3✔
242
                slist = []
3✔
243
                for s in h["__NmapHost__"]["_services"]:
3✔
244
                    cname = "__NmapService__"
3✔
245
                    slist.append(
3✔
246
                        NmapService(
247
                            portid=s[cname]["_portid"],
248
                            protocol=s[cname]["_protocol"],
249
                            state=s[cname]["_state"],
250
                            owner=s[cname]["_owner"],
251
                            service=s[cname]["_service"],
252
                        )
253
                    )
254

255
                nh = NmapHost(
3✔
256
                    starttime=h["__NmapHost__"]["_starttime"],
257
                    endtime=h["__NmapHost__"]["_endtime"],
258
                    address=h["__NmapHost__"]["_address"],
259
                    status=h["__NmapHost__"]["_status"],
260
                    hostnames=h["__NmapHost__"]["_hostnames"],
261
                    extras=h["__NmapHost__"]["_extras"],
262
                    services=slist,
263
                )
264
                hlist.append(nh)
3✔
265
            nreport["_hosts"] = hlist
3✔
266
            nmapobj = NmapReport(nreport)
3✔
267
        return nmapobj
3✔
268

269
    @classmethod
3✔
270
    def __parse_scaninfo(cls, scaninfo_data):
3✔
271
        """
272
        Private method parsing a portion of a nmap scan result.
273
        Receives a <scaninfo> XML tag.
274

275
        :param scaninfo_data: <scaninfo> XML tag from a nmap scan
276
        :type scaninfo_data: xml.ElementTree.Element or a string
277

278
        :return: python dict representing the XML scaninfo tag
279
        """
280

281
        xelement = cls.__format_element(scaninfo_data)
3✔
282
        return cls.__format_attributes(xelement)
3✔
283

284
    @classmethod
3✔
285
    def _parse_xml_host(cls, scanhost_data):
3✔
286
        """
287
        Protected method parsing a portion of a nmap scan result.
288
        Receives a <host> XML tag representing a scanned host with
289
        its services.
290

291
        :param scaninfo_data: <host> XML tag from a nmap scan
292
        :type scaninfo_data: xml.ElementTree.Element or a string
293

294
        :return: NmapHost object
295
        """
296

297
        xelement = cls.__format_element(scanhost_data)
3✔
298
        _host_header = cls.__format_attributes(xelement)
3✔
299
        _hostnames = []
3✔
300
        _services = []
3✔
301
        _status = {}
3✔
302
        _addresses = []
3✔
303
        _host_extras = {}
3✔
304
        extra_tags = [
3✔
305
            "uptime",
306
            "distance",
307
            "tcpsequence",
308
            "ipidsequence",
309
            "tcptssequence",
310
            "trace",
311
            "times",
312
        ]
313
        for xh in xelement:
3✔
314
            if xh.tag == "hostnames":
3✔
315
                for hostname in cls.__parse_hostnames(xh):
3✔
316
                    _hostnames.append(hostname)
3✔
317
            elif xh.tag == "ports":
3✔
318
                ports_dict = cls._parse_xml_ports(xh)
3✔
319
                for port in ports_dict["ports"]:
3✔
320
                    _services.append(port)
3✔
321
                _host_extras["extraports"] = ports_dict["extraports"]
3✔
322
            elif xh.tag == "status":
3✔
323
                _status = cls.__format_attributes(xh)
3✔
324
            elif xh.tag == "address":
3✔
325
                _addresses.append(cls.__format_attributes(xh))
3✔
326
            elif xh.tag == "os":
3✔
327
                _os_extra = cls.__parse_os_fingerprint(xh)
3✔
328
                _host_extras.update({"os": _os_extra})
3✔
329
            elif xh.tag == "hostscript":
3✔
330
                _host_scripts = cls.__parse_host_scripts(xh)
3✔
331
                _host_extras.update({"hostscript": _host_scripts})
3✔
332
            elif xh.tag == "trace":
3✔
333
                _trace = cls.__parse_trace(xh)
3✔
334
                _host_extras.update({"trace": _trace})
3✔
335
            elif xh.tag in extra_tags:
3✔
336
                _host_extras[xh.tag] = cls.__format_attributes(xh)
3✔
337
            # else:
338
            #    print "struct host unknown attr: %s value: %s" %
339
            #           (h.tag, h.get(h.tag))
340
        _stime = _host_header.get("starttime", "")
3✔
341
        _etime = _host_header.get("endtime", "")
3✔
342
        nhost = NmapHost(
3✔
343
            _stime,
344
            _etime,
345
            _addresses,
346
            _status,
347
            _hostnames,
348
            _services,
349
            _host_extras,
350
        )
351
        return nhost
3✔
352

353
    @classmethod
3✔
354
    def __parse_hostnames(cls, scanhostnames_data):
3✔
355
        """
356
        Private method parsing the hostnames list within a <host> XML tag.
357

358
        :param scanhostnames_data: <hostnames> XML tag from a nmap scan
359
        :type scanhostnames_data: xml.ElementTree.Element or a string
360

361
        :return: list of hostnames
362
        """
363

364
        xelement = cls.__format_element(scanhostnames_data)
3✔
365
        hostnames = []
3✔
366
        for hname in xelement:
3✔
367
            if hname.tag == "hostname":
3✔
368
                hostnames.append(hname.get("name"))
3✔
369
        return hostnames
3✔
370

371
    @classmethod
3✔
372
    def _parse_xml_ports(cls, scanports_data):
3✔
373
        """
374
        Protected method parsing the list of scanned services from
375
        a targeted host. This protected method cannot be called directly
376
        with a string. A <ports/> tag can be directly passed to parse()
377
        and the below method will be called and return a list of nmap
378
        scanned services.
379

380
        :param scanports_data: <ports> XML tag from a nmap scan
381
        :type scanports_data: xml.ElementTree.Element or a string
382

383
        :return: list of NmapService
384
        """
385

386
        xelement = cls.__format_element(scanports_data)
3✔
387

388
        rdict = {"ports": [], "extraports": []}
3✔
389
        for xservice in xelement:
3✔
390
            if xservice.tag == "port":
3✔
391
                nport = cls._parse_xml_port(xservice)
3✔
392
                rdict["ports"].append(nport)
3✔
393
            elif xservice.tag == "extraports":
3✔
394
                extraports = cls.__parse_extraports(xservice)
3✔
395
                rdict["extraports"].append(extraports)
3✔
396
        return rdict
3✔
397

398
    @classmethod
3✔
399
    def _parse_xml_port(cls, scanport_data):
3✔
400
        """
401
        Protected method parsing a scanned service from a targeted host.
402
        This protected method cannot be called directly.
403
        A <port/> tag can be directly passed to parse() and the below
404
        method will be called and return a NmapService object
405
        representing the state of the service.
406

407
        :param scanport_data: <port> XML tag from a nmap scan
408
        :type scanport_data: xml.ElementTree.Element or a string
409

410
        :return: NmapService
411
        """
412

413
        xelement = cls.__format_element(scanport_data)
3✔
414

415
        _port = cls.__format_attributes(xelement)
3✔
416
        _portid = _port["portid"] if "portid" in _port else None
3✔
417
        _protocol = _port["protocol"] if "protocol" in _port else None
3✔
418

419
        _state = None
3✔
420
        _service = None
3✔
421
        _owner = None
3✔
422
        _service_scripts = []
3✔
423
        _service_extras = {}
3✔
424
        for xport in xelement:
3✔
425
            if xport.tag == "state":
3✔
426
                _state = cls.__format_attributes(xport)
3✔
427
            elif xport.tag == "service":
3✔
428
                _service = cls.__parse_service(xport)
3✔
429
            elif xport.tag == "owner":
3✔
430
                _owner = cls.__format_attributes(xport)
3✔
431
            elif xport.tag == "script":
3✔
432
                _script_dict = cls.__parse_script(xport)
3✔
433
                _service_scripts.append(_script_dict)
3✔
434
        _service_extras["scripts"] = _service_scripts
3✔
435

436
        if _portid is None or _protocol is None or _state is None:
3✔
437
            raise NmapParserException(
3✔
438
                "XML <port> tag is incomplete. One "
439
                "of the following tags is missing: "
440
                "portid, protocol or state or tag."
441
            )
442

443
        nport = NmapService(
3✔
444
            _portid, _protocol, _state, _service, _owner, _service_extras
445
        )
446
        return nport
3✔
447

448
    @classmethod
3✔
449
    def __parse_service(cls, xserv):
3✔
450
        """
451
        Parse <service> tag to manage CPE object
452
        """
453
        _service = cls.__format_attributes(xserv)
3✔
454
        _cpelist = []
3✔
455
        for _servnode in xserv:
3✔
456
            if _servnode.tag == "cpe":
3✔
457
                _cpe_string = _servnode.text
3✔
458
                _cpelist.append(_cpe_string)
3✔
459
        _service["cpelist"] = _cpelist
3✔
460
        return _service
3✔
461

462
    @classmethod
3✔
463
    def __parse_extraports(cls, extraports_data):
3✔
464
        """
465
        Private method parsing the data from extra scanned ports.
466
        X extraports were in state "closed" server returned "conn-refused"
467
        tag: <extraports>
468

469
        :param extraports_data: XML data for extraports
470
        :type extraports_data: xml.ElementTree.Element or a string
471

472
        :return: python dict with following keys: state, count, reasons
473
        """
474
        rdict = {"state": "", "count": "", "extrareasons": []}
3✔
475
        xelement = cls.__format_element(extraports_data)
3✔
476
        extraports_dict = cls.__format_attributes(xelement)
3✔
477

478
        rdict["state"] = extraports_dict.get("state", None)
3✔
479
        rdict["count"] = extraports_dict.get("count", None)
3✔
480
        for xelt in xelement:
3✔
481
            if xelt.tag == "extrareasons":
3✔
482
                extrareasons_dict = cls.__format_attributes(xelt)
3✔
483
                rdict["extrareasons"].append(extrareasons_dict)
3✔
484
        return rdict
3✔
485

486
    @classmethod
3✔
487
    def __parse_script_table(cls, script_table):
3✔
488
        """
489
        Private method parsing a table from NSE scripts output
490

491
        :param sccript_table: poertion of XML containing the table
492
        :type script_table: xml.ElementTree.Element
493

494
        :return: python dict of table structure
495
        """
496
        tdict = {}
3✔
497
        for telem in script_table:
3✔
498
            tkey = telem.get("key")
3✔
499
            if telem.tag == "elem":
3✔
500
                if tkey in tdict:
3✔
501
                    if not isinstance(tdict[tkey], list):
3✔
502
                        tdict[tkey] = [tdict[tkey]]
3✔
503
                    tdict[tkey].append(telem.text)
3✔
504
                else:
505
                    tdict[tkey] = telem.text
3✔
506
            elif telem.tag == "table":
3✔
507
                stdict = cls.__parse_script_table(telem)
3✔
508

509
                # Handle duplicate table keys
510
                if tkey in tdict:
3✔
511
                    if not isinstance(tdict[tkey], list):
3✔
512
                        tdict[tkey] = [tdict[tkey]]
3✔
513
                    tdict[tkey].append(stdict)
3✔
514
                else:
515
                    tdict[tkey] = stdict
3✔
516
        return tdict
3✔
517

518
    @classmethod
3✔
519
    def __parse_script(cls, script_data):
3✔
520
        """
521
        Private method parsing the data from NSE scripts output
522

523
        :param script_data: portion of XML describing the results of the
524
        script data
525
        :type script_data: xml.ElementTree.Element or a string
526

527
        :return: python dict holding scripts output
528
        """
529
        _script_dict = cls.__format_attributes(script_data)
3✔
530

531
        _elt_dict = {}
3✔
532
        for script_elem in script_data:
3✔
533
            if script_elem.tag == "elem":
3✔
534
                _elt_dict.update({script_elem.get("key"): script_elem.text})
3✔
535
            elif script_elem.tag == "table":
3✔
536
                tdict = cls.__parse_script_table(script_elem)
3✔
537
                # Handle duplicate table keys
538
                skey = script_elem.get("key")
3✔
539
                if skey in _elt_dict:
3✔
540
                    if not isinstance(_elt_dict[skey], list):
3✔
541
                        _elt_dict[skey] = [_elt_dict[skey]]
3✔
542
                    _elt_dict[skey].append(tdict)
3✔
543
                else:
544
                    _elt_dict[skey] = tdict
3✔
545
        _script_dict["elements"] = _elt_dict
3✔
546
        return _script_dict
3✔
547

548
    @classmethod
3✔
549
    def __parse_host_scripts(cls, scripts_data):
3✔
550
        """
551
        Private method parsing the data from scripts affecting
552
        the target host.
553
        Contents of <hostscript> is returned as a list of dict.
554

555
        :param scripts_data: portion of XML describing the results of the
556
        scripts data
557
        :type scripts_data: xml.ElementTree.Element or a string
558

559
        :return: python list holding scripts output in a dict
560
        """
561
        _host_scripts = []
3✔
562
        for xscript in scripts_data:
3✔
563
            if xscript.tag == "script":
3✔
564
                _script_dict = cls.__parse_script(xscript)
3✔
565
            _host_scripts.append(_script_dict)
3✔
566
        return _host_scripts
3✔
567

568
    @classmethod
3✔
569
    def __parse_os_fingerprint(cls, os_data):
3✔
570
        """
571
        Private method parsing the data from an OS fingerprint (-O).
572
        Contents of <os> is returned as a dict.
573

574
        :param os_data: portion of XML describing the results of the
575
        os fingerprinting attempt
576
        :type os_data: xml.ElementTree.Element or a string
577

578
        :return: python dict representing the XML os tag
579
        """
580
        rdict = {}
3✔
581
        xelement = cls.__format_element(os_data)
3✔
582

583
        os_class_probability = []
3✔
584
        os_match_probability = []
3✔
585
        os_ports_used = []
3✔
586
        os_fingerprints = []
3✔
587
        for xos in xelement:
3✔
588
            # for nmap xml version < 1.04, osclass is not
589
            # embedded in osmatch
590
            if xos.tag == "osclass":
3✔
591
                os_class_proba = cls.__parse_osclass(xos)
3✔
592
                os_class_probability.append(os_class_proba)
3✔
593
            elif xos.tag == "osmatch":
3✔
594
                os_match_proba = cls.__parse_osmatch(xos)
3✔
595
                os_match_probability.append(os_match_proba)
3✔
596
            elif xos.tag == "portused":
3✔
597
                os_portused = cls.__format_attributes(xos)
3✔
598
                os_ports_used.append(os_portused)
3✔
599
            elif xos.tag == "osfingerprint":
3✔
600
                os_fp_dict = cls.__format_attributes(xos)
3✔
601
                os_fingerprints.append(os_fp_dict)
3✔
602

603
        rdict["osmatches"] = os_match_probability
3✔
604
        rdict["osclasses"] = os_class_probability
3✔
605
        rdict["ports_used"] = os_ports_used
3✔
606
        rdict["osfingerprints"] = os_fingerprints
3✔
607

608
        return rdict
3✔
609

610
    @classmethod
3✔
611
    def __parse_osmatch(cls, osmatch_data):
3✔
612
        """
613
        This methods parses osmatch data and returns a dict. Depending
614
        on the nmap xml version, osmatch could contain an osclass
615
        dict.
616

617
        :param osmatch_data: <osmatch> XML tag from a nmap scan
618
        :type osmatch_data: xml.ElementTree.Element or a string
619

620
        :return: python dict representing the XML osmatch tag
621
        """
622
        rdict = {}
3✔
623
        xelement = cls.__format_element(osmatch_data)
3✔
624
        rdict["osmatch"] = cls.__format_attributes(xelement)
3✔
625
        rdict["osclasses"] = []
3✔
626
        for xmltag in xelement:
3✔
627
            if xmltag.tag == "osclass":
3✔
628
                _osclass_dict = cls.__parse_osclass(xmltag)
3✔
629
                rdict["osclasses"].append(_osclass_dict)
3✔
630
            else:
UNCOV
631
                exmsg = "Unexcepted node in <osmatch>: {0}".format(xmltag.tag)
×
UNCOV
632
                raise NmapParserException(exmsg)
×
633
        return rdict
3✔
634

635
    @classmethod
3✔
636
    def __parse_osclass(cls, osclass_data):
3✔
637
        """
638
        This methods parses osclass data and returns a dict. Depending
639
        on the nmap xml version, osclass could contain a cpe
640
        dict.
641

642
        :param osclass_data: <osclass> XML tag from a nmap scan
643
        :type osclass_data: xml.ElementTree.Element or a string
644

645
        :return: python dict representing the XML osclass tag
646
        """
647
        rdict = {}
3✔
648
        xelement = cls.__format_element(osclass_data)
3✔
649
        rdict["osclass"] = cls.__format_attributes(xelement)
3✔
650
        rdict["cpe"] = []
3✔
651
        for xmltag in xelement:
3✔
652
            if xmltag.tag == "cpe":
3✔
653
                _cpe_string = xmltag.text
3✔
654
                rdict["cpe"].append(_cpe_string)
3✔
655
            else:
UNCOV
656
                exmsg = "Unexcepted node in <osclass>: {0}".format(xmltag.tag)
×
UNCOV
657
                raise NmapParserException(exmsg)
×
658
        return rdict
3✔
659

660
    @classmethod
3✔
661
    def __parse_runstats(cls, scanrunstats_data):
3✔
662
        """
663
        Private method parsing a portion of a nmap scan result.
664
        Receives a <runstats> XML tag.
665

666
        :param scanrunstats_data: <runstats> XML tag from a nmap scan
667
        :type scanrunstats_data: xml.ElementTree.Element or a string
668

669
        :return: python dict representing the XML runstats tag
670
        """
671

672
        xelement = cls.__format_element(scanrunstats_data)
3✔
673

674
        rdict = {}
3✔
675
        for xmltag in xelement:
3✔
676
            if xmltag.tag in ["finished", "hosts"]:
3✔
677
                rdict[xmltag.tag] = cls.__format_attributes(xmltag)
3✔
678
            else:
UNCOV
679
                exmsg = "Unexcepted node in <runstats>: {0}".format(xmltag.tag)
×
UNCOV
680
                raise NmapParserException(exmsg)
×
681

682
        return rdict
3✔
683

684
    @classmethod
3✔
685
    def __parse_trace(cls, scantrace_data):
3✔
686
        """
687
        Private method parsing a portion of a nmap scan result.
688
        Receives a <trace> XML tag.
689

690
        :param scantrace_data: <trace> XML tag from a nmap scan
691
        :type scantrace_data: xml.ElementTree.Element or a string
692

693
        :return: python dict representing the XML trace tag
694
        """
695

696
        xelement = cls.__format_element(scantrace_data)
3✔
697
        _trace_attrs = cls.__format_attributes(xelement)
3✔
698

699
        rdict = {}
3✔
700

701
        if "proto" in _trace_attrs:
3✔
UNCOV
702
            rdict["proto"] = _trace_attrs["proto"]
×
703

704
        if "port" in _trace_attrs:
3✔
UNCOV
705
            rdict["port"] = _trace_attrs["port"]
×
706

707
        rdict["hops"] = []
3✔
708
        for xmltag in xelement:
3✔
709
            if xmltag.tag in ["hop"]:
3✔
710
                rdict["hops"].append(cls.__format_attributes(xmltag))
3✔
711
            else:
UNCOV
712
                exmsg = "Unexcepted node in <trace>: {0}".format(xmltag.tag)
×
UNCOV
713
                raise NmapParserException(exmsg)
×
714

715
        return rdict
3✔
716

717
    @staticmethod
3✔
718
    def __format_element(elt_data):
3✔
719
        """
720
        Private method which ensures that a XML portion to be parsed is
721
        of type xml.etree.ElementTree.Element.
722
        If elt_data is a string, then it is converted to an
723
        XML Element type.
724

725
        :param elt_data: XML Element to be parsed or string
726
        to be converted to a XML Element
727

728
        :return: Element
729
        """
730
        if isinstance(elt_data, str):
3✔
UNCOV
731
            try:
×
UNCOV
732
                xelement = ET.fromstring(elt_data)
×
UNCOV
733
            except Exception as e:
×
UNCOV
734
                raise NmapParserException(
×
735
                    "Error while trying "
736
                    "to instanciate XML Element from "
737
                    "string {0} - {1}".format(elt_data, e)
738
                )
739
        elif et_iselement(elt_data):
3✔
740
            xelement = elt_data
3✔
741
        else:
UNCOV
742
            raise NmapParserException(
×
743
                "Error while trying to parse supplied "
744
                "data: unsupported format"
745
            )
746
        return xelement
3✔
747

748
    @staticmethod
3✔
749
    def __format_attributes(elt_data):
3✔
750
        """
751
        Private method which converts a single XML tag to a python dict.
752
        It also checks that the elt_data given as argument is of type
753
        xml.etree.ElementTree.Element
754

755
        :param elt_data: XML Element to be parsed or string
756
        to be converted to a XML Element
757

758
        :return: Element
759
        """
760

761
        rval = {}
3✔
762
        if not et_iselement(elt_data):
3✔
UNCOV
763
            raise NmapParserException(
×
764
                "Error while trying to parse supplied "
765
                "data attributes: format is not XML or "
766
                "XML tag is empty"
767
            )
768
        try:
3✔
769
            for dkey in elt_data.keys():
3✔
770
                rval[dkey] = elt_data.get(dkey)
3✔
771
                if rval[dkey] is None:
3✔
UNCOV
772
                    raise NmapParserException(
×
773
                        "Error while trying to build-up "
774
                        "element attributes: empty "
775
                        "attribute {0}".format(dkey)
776
                    )
UNCOV
777
        except Exception:
×
UNCOV
778
            raise
×
779
        return rval
3✔
780

781

782
class NmapParserException(Exception):
3✔
783
    def __init__(self, msg):
3✔
784
        self.msg = msg
3✔
785

786
    def __str__(self):
3✔
787
        return self.msg
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc