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

savon-noir / python-libnmap / 12857916376

19 Jan 2025 10:59PM UTC coverage: 72.708% (+0.9%) from 71.843%
12857916376

push

github

savon-noir
fix: gh actions fix bllint issue

1745 of 2400 relevant lines covered (72.71%)

2.86 hits per line

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

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

3

4
try:
4✔
5
    import defusedxml.ElementTree as ET
4✔
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
4✔
13

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

16

17
class NmapParser(object):
4✔
18
    @classmethod
4✔
19
    def parse(cls, nmap_data=None, data_type="XML", incomplete=False):
4✔
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
4✔
48
        if data_type == "XML":
4✔
49
            nmapobj = cls._parse_xml(nmap_data, incomplete)
4✔
50
        else:
51
            raise NmapParserException(
4✔
52
                "Unknown data type provided. "
53
                "Please check documentation for "
54
                "supported data types."
55
            )
56
        return nmapobj
4✔
57

58
    @classmethod
4✔
59
    def _parse_xml(cls, nmap_data=None, incomplete=False):
4✔
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:
4✔
90
            raise NmapParserException(
4✔
91
                "No report data to parse: please "
92
                "provide a valid XML nmap report"
93
            )
94
        elif not isinstance(nmap_data, str):
4✔
95
            raise NmapParserException(
4✔
96
                "wrong nmap_data type given as argument: cannot parse data"
97
            )
98

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

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

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

123
    @classmethod
4✔
124
    def _parse_xml_report(cls, root=None):
4✔
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 = {
4✔
136
            "_nmaprun": {},
137
            "_scaninfo": {},
138
            "_hosts": [],
139
            "_runstats": {},
140
        }
141

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

147
        nmap_scan["_nmaprun"] = cls.__format_attributes(root)
4✔
148
        for el in root:
4✔
149
            if el.tag == "scaninfo":
4✔
150
                nmap_scan["_scaninfo"] = cls.__parse_scaninfo(el)
4✔
151
            elif el.tag == "host":
4✔
152
                nmap_scan["_hosts"].append(cls._parse_xml_host(el))
4✔
153
            elif el.tag == "runstats":
4✔
154
                nmap_scan["_runstats"] = cls.__parse_runstats(el)
4✔
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)
4✔
160

161
    @classmethod
4✔
162
    def parse_fromstring(cls, nmap_data, data_type="XML", incomplete=False):
4✔
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):
4✔
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)
4✔
188

189
    @classmethod
4✔
190
    def parse_fromfile(
4✔
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:
4✔
213
            with open(nmap_report_path, "r") as fileobj:
4✔
214
                fdata = fileobj.read()
4✔
215
                rval = cls.parse(fdata, data_type, incomplete)
4✔
216
        except IOError:
4✔
217
            raise
×
218
        return rval
4✔
219

220
    @classmethod
4✔
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 = {}
4✔
235
        if list(rdict.keys())[0] == "__NmapReport__":
4✔
236
            r = rdict["__NmapReport__"]
4✔
237
            nreport["_runstats"] = r["_runstats"]
4✔
238
            nreport["_scaninfo"] = r["_scaninfo"]
4✔
239
            nreport["_nmaprun"] = r["_nmaprun"]
4✔
240
            hlist = []
4✔
241
            for h in r["_hosts"]:
4✔
242
                slist = []
4✔
243
                for s in h["__NmapHost__"]["_services"]:
4✔
244
                    cname = "__NmapService__"
4✔
245
                    slist.append(
4✔
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(
4✔
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)
4✔
265
            nreport["_hosts"] = hlist
4✔
266
            nmapobj = NmapReport(nreport)
4✔
267
        return nmapobj
4✔
268

269
    @classmethod
4✔
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)
4✔
282
        return cls.__format_attributes(xelement)
4✔
283

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

353
    @classmethod
4✔
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)
4✔
365
        hostnames = []
4✔
366
        for hname in xelement:
4✔
367
            if hname.tag == "hostname":
4✔
368
                hostnames.append(hname.get("name"))
4✔
369
        return hostnames
4✔
370

371
    @classmethod
4✔
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)
4✔
387

388
        rdict = {"ports": [], "extraports": None}
4✔
389
        for xservice in xelement:
4✔
390
            if xservice.tag == "port":
4✔
391
                nport = cls._parse_xml_port(xservice)
4✔
392
                rdict["ports"].append(nport)
4✔
393
            elif xservice.tag == "extraports":
4✔
394
                extraports = cls.__parse_extraports(xservice)
4✔
395
                rdict["extraports"] = extraports
4✔
396
            # else:
397
            #    print "struct port unknown attr: %s value: %s" %
398
            #           (h.tag, h.get(h.tag))
399
        return rdict
4✔
400

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

410
        :param scanport_data: <port> XML tag from a nmap scan
411
        :type scanport_data: xml.ElementTree.Element or a string
412

413
        :return: NmapService
414
        """
415

416
        xelement = cls.__format_element(scanport_data)
4✔
417

418
        _port = cls.__format_attributes(xelement)
4✔
419
        _portid = _port["portid"] if "portid" in _port else None
4✔
420
        _protocol = _port["protocol"] if "protocol" in _port else None
4✔
421

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

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

446
        nport = NmapService(
4✔
447
            _portid, _protocol, _state, _service, _owner, _service_extras
448
        )
449
        return nport
4✔
450

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

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

472
        :param extraports_data: XML data for extraports
473
        :type extraports_data: xml.ElementTree.Element or a string
474

475
        :return: python dict with following keys: state, count, reason
476
        """
477
        rdict = {"state": "", "count": "", "reasons": []}
4✔
478
        xelement = cls.__format_element(extraports_data)
4✔
479
        extraports_dict = cls.__format_attributes(xelement)
4✔
480

481
        if "state" in extraports_dict:
4✔
482
            rdict["state"] = extraports_dict
4✔
483
        if "count" in extraports_dict:
4✔
484
            rdict["count"] = extraports_dict
4✔
485
        for xelt in xelement:
4✔
486
            if xelt.tag == "extrareasons":
4✔
487
                extrareasons_dict = cls.__format_attributes(xelt)
4✔
488
                rdict["reasons"].append(extrareasons_dict)
4✔
489
        return rdict
4✔
490

491
    @classmethod
4✔
492
    def __parse_script_table(cls, script_table):
3✔
493
        """
494
        Private method parsing a table from NSE scripts output
495

496
        :param sccript_table: poertion of XML containing the table
497
        :type script_table: xml.ElementTree.Element
498

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

514
                # Handle duplicate table keys
515
                if tkey in tdict:
4✔
516
                    if not isinstance(tdict[tkey], list):
4✔
517
                        tdict[tkey] = [tdict[tkey]]
4✔
518
                    tdict[tkey].append(stdict)
4✔
519
                else:
520
                    tdict[tkey] = stdict
4✔
521
        return tdict
4✔
522

523
    @classmethod
4✔
524
    def __parse_script(cls, script_data):
3✔
525
        """
526
        Private method parsing the data from NSE scripts output
527

528
        :param script_data: portion of XML describing the results of the
529
        script data
530
        :type script_data: xml.ElementTree.Element or a string
531

532
        :return: python dict holding scripts output
533
        """
534
        _script_dict = cls.__format_attributes(script_data)
4✔
535

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

553
    @classmethod
4✔
554
    def __parse_host_scripts(cls, scripts_data):
3✔
555
        """
556
        Private method parsing the data from scripts affecting
557
        the target host.
558
        Contents of <hostscript> is returned as a list of dict.
559

560
        :param scripts_data: portion of XML describing the results of the
561
        scripts data
562
        :type scripts_data: xml.ElementTree.Element or a string
563

564
        :return: python list holding scripts output in a dict
565
        """
566
        _host_scripts = []
4✔
567
        for xscript in scripts_data:
4✔
568
            if xscript.tag == "script":
4✔
569
                _script_dict = cls.__parse_script(xscript)
4✔
570
            _host_scripts.append(_script_dict)
4✔
571
        return _host_scripts
4✔
572

573
    @classmethod
4✔
574
    def __parse_os_fingerprint(cls, os_data):
3✔
575
        """
576
        Private method parsing the data from an OS fingerprint (-O).
577
        Contents of <os> is returned as a dict.
578

579
        :param os_data: portion of XML describing the results of the
580
        os fingerprinting attempt
581
        :type os_data: xml.ElementTree.Element or a string
582

583
        :return: python dict representing the XML os tag
584
        """
585
        rdict = {}
4✔
586
        xelement = cls.__format_element(os_data)
4✔
587

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

608
        rdict["osmatches"] = os_match_probability
4✔
609
        rdict["osclasses"] = os_class_probability
4✔
610
        rdict["ports_used"] = os_ports_used
4✔
611
        rdict["osfingerprints"] = os_fingerprints
4✔
612

613
        return rdict
4✔
614

615
    @classmethod
4✔
616
    def __parse_osmatch(cls, osmatch_data):
3✔
617
        """
618
        This methods parses osmatch data and returns a dict. Depending
619
        on the nmap xml version, osmatch could contain an osclass
620
        dict.
621

622
        :param osmatch_data: <osmatch> XML tag from a nmap scan
623
        :type osmatch_data: xml.ElementTree.Element or a string
624

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

640
    @classmethod
4✔
641
    def __parse_osclass(cls, osclass_data):
3✔
642
        """
643
        This methods parses osclass data and returns a dict. Depending
644
        on the nmap xml version, osclass could contain a cpe
645
        dict.
646

647
        :param osclass_data: <osclass> XML tag from a nmap scan
648
        :type osclass_data: xml.ElementTree.Element or a string
649

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

665
    @classmethod
4✔
666
    def __parse_runstats(cls, scanrunstats_data):
3✔
667
        """
668
        Private method parsing a portion of a nmap scan result.
669
        Receives a <runstats> XML tag.
670

671
        :param scanrunstats_data: <runstats> XML tag from a nmap scan
672
        :type scanrunstats_data: xml.ElementTree.Element or a string
673

674
        :return: python dict representing the XML runstats tag
675
        """
676

677
        xelement = cls.__format_element(scanrunstats_data)
4✔
678

679
        rdict = {}
4✔
680
        for xmltag in xelement:
4✔
681
            if xmltag.tag in ["finished", "hosts"]:
4✔
682
                rdict[xmltag.tag] = cls.__format_attributes(xmltag)
4✔
683
            else:
684
                exmsg = "Unexcepted node in <runstats>: {0}".format(xmltag.tag)
×
685
                raise NmapParserException(exmsg)
×
686

687
        return rdict
4✔
688

689
    @classmethod
4✔
690
    def __parse_trace(cls, scantrace_data):
3✔
691
        """
692
        Private method parsing a portion of a nmap scan result.
693
        Receives a <trace> XML tag.
694

695
        :param scantrace_data: <trace> XML tag from a nmap scan
696
        :type scantrace_data: xml.ElementTree.Element or a string
697

698
        :return: python dict representing the XML trace tag
699
        """
700

701
        xelement = cls.__format_element(scantrace_data)
4✔
702
        _trace_attrs = cls.__format_attributes(xelement)
4✔
703

704
        rdict = {}
4✔
705

706
        if "proto" in _trace_attrs:
4✔
707
            rdict["proto"] = _trace_attrs["proto"]
×
708

709
        if "port" in _trace_attrs:
4✔
710
            rdict["port"] = _trace_attrs["port"]
×
711

712
        rdict["hops"] = []
4✔
713
        for xmltag in xelement:
4✔
714
            if xmltag.tag in ["hop"]:
4✔
715
                rdict["hops"].append(cls.__format_attributes(xmltag))
4✔
716
            else:
717
                exmsg = "Unexcepted node in <trace>: {0}".format(xmltag.tag)
×
718
                raise NmapParserException(exmsg)
×
719

720
        return rdict
4✔
721

722
    @staticmethod
4✔
723
    def __format_element(elt_data):
3✔
724
        """
725
        Private method which ensures that a XML portion to be parsed is
726
        of type xml.etree.ElementTree.Element.
727
        If elt_data is a string, then it is converted to an
728
        XML Element type.
729

730
        :param elt_data: XML Element to be parsed or string
731
        to be converted to a XML Element
732

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

753
    @staticmethod
4✔
754
    def __format_attributes(elt_data):
3✔
755
        """
756
        Private method which converts a single XML tag to a python dict.
757
        It also checks that the elt_data given as argument is of type
758
        xml.etree.ElementTree.Element
759

760
        :param elt_data: XML Element to be parsed or string
761
        to be converted to a XML Element
762

763
        :return: Element
764
        """
765

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

786

787
class NmapParserException(Exception):
4✔
788
    def __init__(self, msg):
4✔
789
        self.msg = msg
4✔
790

791
    def __str__(self):
4✔
792
        return self.msg
4✔
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

© 2025 Coveralls, Inc