1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# ***********************IMPORTANT NMAP LICENSE TERMS************************
5# *                                                                         *
6# * The Nmap Security Scanner is (C) 1996-2020 Insecure.Com LLC ("The Nmap  *
7# * Project"). Nmap is also a registered trademark of the Nmap Project.     *
8# *                                                                         *
9# * This program is distributed under the terms of the Nmap Public Source   *
10# * License (NPSL). The exact license text applying to a particular Nmap    *
11# * release or source code control revision is contained in the LICENSE     *
12# * file distributed with that version of Nmap or source code control       *
13# * revision. More Nmap copyright/legal information is available from       *
14# * https://nmap.org/book/man-legal.html, and further information on the    *
15# * NPSL license itself can be found at https://nmap.org/npsl. This header  *
16# * summarizes some key points from the Nmap license, but is no substitute  *
17# * for the actual license text.                                            *
18# *                                                                         *
19# * Nmap is generally free for end users to download and use themselves,    *
20# * including commercial use. It is available from https://nmap.org.        *
21# *                                                                         *
22# * The Nmap license generally prohibits companies from using and           *
23# * redistributing Nmap in commercial products, but we sell a special Nmap  *
24# * OEM Edition with a more permissive license and special features for     *
25# * this purpose. See https://nmap.org/oem                                  *
26# *                                                                         *
27# * If you have received a written Nmap license agreement or contract       *
28# * stating terms other than these (such as an Nmap OEM license), you may   *
29# * choose to use and redistribute Nmap under those terms instead.          *
30# *                                                                         *
31# * The official Nmap Windows builds include the Npcap software             *
32# * (https://npcap.org) for packet capture and transmission. It is under    *
33# * separate license terms which forbid redistribution without special      *
34# * permission. So the official Nmap Windows builds may not be              *
35# * redistributed without special permission (such as an Nmap OEM           *
36# * license).                                                               *
37# *                                                                         *
38# * Source is provided to this software because we believe users have a     *
39# * right to know exactly what a program is going to do before they run it. *
40# * This also allows you to audit the software for security holes.          *
41# *                                                                         *
42# * Source code also allows you to port Nmap to new platforms, fix bugs,    *
43# * and add new features.  You are highly encouraged to submit your         *
44# * changes as a Github PR or by email to the dev@nmap.org mailing list     *
45# * for possible incorporation into the main distribution. Unless you       *
46# * specify otherwise, it is understood that you are offering us very       *
47# * broad rights to use your submissions as described in the Nmap Public    *
48# * Source License Contributor Agreement. This is important because we      *
49# * fund the project by selling licenses with various terms, and also       *
50# * because the inability to relicense code has caused devastating          *
51# * problems for other Free Software projects (such as KDE and NASM).       *
52# *                                                                         *
53# * The free version of Nmap is distributed in the hope that it will be     *
54# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of  *
55# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties,        *
56# * indemnification and commercial support are all available through the    *
57# * Npcap OEM program--see https://nmap.org/oem.                            *
58# *                                                                         *
59# ***************************************************************************/
60
61import re
62
63from types import StringTypes
64from ConfigParser import DuplicateSectionError, NoSectionError, NoOptionError
65from ConfigParser import Error as ConfigParser_Error
66
67from zenmapCore.Paths import Path
68from zenmapCore.UmitLogging import log
69from zenmapCore.UmitConfigParser import UmitConfigParser
70import zenmapCore.I18N  # lgtm[py/unused-import]
71
72# This is the global configuration parser object that represents the contents
73# of zenmap.conf. It should be initialized once by the application. Most
74# interaction with the global parser is done by other classes in this file,
75# like SearchConfig, that wrap specific configuration sections.
76config_parser = UmitConfigParser()
77
78# Check if running on Maemo
79MAEMO = False
80try:
81    import hildon
82    MAEMO = True
83except ImportError:
84    pass
85
86
87def is_maemo():
88    return MAEMO
89
90
91class SearchConfig(UmitConfigParser, object):
92    section_name = "search"
93
94    def __init__(self):
95        if not config_parser.has_section(self.section_name):
96            self.create_section()
97
98    def save_changes(self):
99        config_parser.save_changes()
100
101    def create_section(self):
102        config_parser.add_section(self.section_name)
103        self.directory = ""
104        self.file_extension = "xml"
105        self.save_time = "60;days"
106        self.store_results = True
107        self.search_db = True
108
109    def _get_it(self, p_name, default):
110        return config_parser.get(self.section_name, p_name, default)
111
112    def _set_it(self, p_name, value):
113        config_parser.set(self.section_name, p_name, value)
114
115    def boolean_sanity(self, attr):
116        if attr is True or \
117           attr == "True" or \
118           attr == "true" or \
119           attr == "1":
120
121            return 1
122
123        return 0
124
125    def get_directory(self):
126        return self._get_it("directory", "")
127
128    def set_directory(self, directory):
129        self._set_it("directory", directory)
130
131    def get_file_extension(self):
132        return self._get_it("file_extension", "xml").split(";")
133
134    def set_file_extension(self, file_extension):
135        if isinstance(file_extension, list):
136            self._set_it("file_extension", ";".join(file_extension))
137        elif isinstance(file_extension, StringTypes):
138            self._set_it("file_extension", file_extension)
139
140    def get_save_time(self):
141        return self._get_it("save_time", "60;days").split(";")
142
143    def set_save_time(self, save_time):
144        if isinstance(save_time, list):
145            self._set_it("save_time", ";".join(save_time))
146        elif isinstance(save_time, StringTypes):
147            self._set_it("save_time", save_time)
148
149    def get_store_results(self):
150        return self.boolean_sanity(self._get_it("store_results", True))
151
152    def set_store_results(self, store_results):
153        self._set_it("store_results", self.boolean_sanity(store_results))
154
155    def get_search_db(self):
156        return self.boolean_sanity(self._get_it("search_db", True))
157
158    def set_search_db(self, search_db):
159        self._set_it("search_db", self.boolean_sanity(search_db))
160
161    def get_converted_save_time(self):
162        try:
163            return int(self.save_time[0]) * self.time_list[self.save_time[1]]
164        except Exception:
165            # If something goes wrong, return a save time of 60 days
166            return 60 * 60 * 24 * 60
167
168    def get_time_list(self):
169        # Time as key, seconds a value
170        return {"hours": 60 * 60,
171                "days": 60 * 60 * 24,
172                "weeks": 60 * 60 * 24 * 7,
173                "months": 60 * 60 * 24 * 7 * 30,
174                "years": 60 * 60 * 24 * 7 * 30 * 12,
175                "minutes": 60,
176                "seconds": 1}
177
178    directory = property(get_directory, set_directory)
179    file_extension = property(get_file_extension, set_file_extension)
180    save_time = property(get_save_time, set_save_time)
181    store_results = property(get_store_results, set_store_results)
182    search_db = property(get_search_db, set_search_db)
183    converted_save_time = property(get_converted_save_time)
184    time_list = property(get_time_list)
185
186
187class Profile(UmitConfigParser, object):
188    """This class represents not just one profile, but a whole collection of
189    them found in a config file such as scan_profiles.usp. The methods
190    therefore all take an argument that is the name of the profile to work
191    on."""
192
193    def __init__(self, user_profile=None, *args):
194        UmitConfigParser.__init__(self, *args)
195
196        try:
197            if not user_profile:
198                user_profile = Path.scan_profile
199
200            self.read(user_profile)
201        except ConfigParser_Error as e:
202            # No scan profiles found is not a reason to crash.
203            self.add_profile(_("Profiles not found"),
204                    command="nmap",
205                    description=_("The {} file is missing or corrupted"
206                        ).format(user_profile))
207
208        self.attributes = {}
209
210    def _get_it(self, profile, attribute):
211        if self._verify_profile(profile):
212            return self.get(profile, attribute)
213        return ""
214
215    def _set_it(self, profile, attribute, value=''):
216        if self._verify_profile(profile):
217            return self.set(profile, attribute, value)
218
219    def add_profile(self, profile_name, **attributes):
220        """Add a profile with the given name and attributes to the collection
221        of profiles. If a profile with the same name exists, it is not
222        overwritten, and the method returns immediately. The backing file for
223        the profiles is automatically updated."""
224
225        log.debug(">>> Add Profile '%s': %s" % (profile_name, attributes))
226
227        try:
228            self.add_section(profile_name)
229        except DuplicateSectionError:
230            return None
231
232        # Set each of the attributes ("command", "description") in the
233        # ConfigParser.
234        for attr in attributes:
235            self._set_it(profile_name, attr, attributes[attr])
236
237        self.save_changes()
238
239    def remove_profile(self, profile_name):
240        try:
241            self.remove_section(profile_name)
242        except Exception:
243            pass
244        self.save_changes()
245
246    def _verify_profile(self, profile_name):
247        if profile_name not in self.sections():
248            return False
249        return True
250
251
252class WindowConfig(UmitConfigParser, object):
253    section_name = "window"
254
255    default_x = 0
256    default_y = 0
257    default_width = -1
258    default_height = 650
259
260    def __init__(self):
261        if not config_parser.has_section(self.section_name):
262            self.create_section()
263
264    def save_changes(self):
265        config_parser.save_changes()
266
267    def create_section(self):
268        config_parser.add_section(self.section_name)
269        self.x = self.default_x
270        self.y = self.default_y
271        self.width = self.default_width
272        self.height = self.default_height
273
274    def _get_it(self, p_name, default):
275        return config_parser.get(self.section_name, p_name, default)
276
277    def _set_it(self, p_name, value):
278        config_parser.set(self.section_name, p_name, value)
279
280    def get_x(self):
281        try:
282            value = int(self._get_it("x", self.default_x))
283        except (ValueError, NoOptionError):
284            value = self.default_x
285        except TypeError as e:
286            v = self._get_it("x", self.default_x)
287            log.exception("Trouble parsing x value as int: %s",
288                    repr(v), exc_info=e)
289            value = self.default_x
290        return value
291
292    def set_x(self, x):
293        self._set_it("x", "%d" % x)
294
295    def get_y(self):
296        try:
297            value = int(self._get_it("y", self.default_y))
298        except (ValueError, NoOptionError):
299            value = self.default_y
300        except TypeError as e:
301            v = self._get_it("y", self.default_y)
302            log.exception("Trouble parsing y value as int: %s",
303                    repr(v), exc_info=e)
304            value = self.default_y
305        return value
306
307    def set_y(self, y):
308        self._set_it("y", "%d" % y)
309
310    def get_width(self):
311        try:
312            value = int(self._get_it("width", self.default_width))
313        except (ValueError, NoOptionError):
314            value = self.default_width
315        except TypeError as e:
316            v = self._get_it("width", self.default_width)
317            log.exception("Trouble parsing width value as int: %s",
318                    repr(v), exc_info=e)
319            value = self.default_width
320
321        if not (value >= -1):
322            value = self.default_width
323
324        return value
325
326    def set_width(self, width):
327        self._set_it("width", "%d" % width)
328
329    def get_height(self):
330        try:
331            value = int(self._get_it("height", self.default_height))
332        except (ValueError, NoOptionError):
333            value = self.default_height
334        except TypeError as e:
335            v = self._get_it("height", self.default_height)
336            log.exception("Trouble parsing y value as int: %s",
337                    repr(v), exc_info=e)
338            value = self.default_height
339
340        if not (value >= -1):
341            value = self.default_height
342
343        return value
344
345    def set_height(self, height):
346        self._set_it("height", "%d" % height)
347
348    x = property(get_x, set_x)
349    y = property(get_y, set_y)
350    width = property(get_width, set_width)
351    height = property(get_height, set_height)
352
353
354class CommandProfile (Profile, object):
355    """This class is a wrapper around Profile that provides accessors for the
356    attributes of a profile: command and description"""
357    def __init__(self, user_profile=None):
358        Profile.__init__(self, user_profile)
359
360    def get_command(self, profile):
361        command_string = self._get_it(profile, 'command')
362        # Corrupted config file can include multiple commands.
363        # Take the first one.
364        if isinstance(command_string, list):
365            command_string = command_string[0]
366        if not hasattr(command_string, "endswith"):
367            return "nmap"
368        # Old versions of Zenmap used to append "%s" to commands and use that
369        # to substitute the target. Ignore it if present.
370        if command_string.endswith("%s"):
371            command_string = command_string[:-len("%s")]
372        return command_string
373
374    def get_description(self, profile):
375        desc = self._get_it(profile, 'description')
376        if isinstance(desc, list):
377            desc = " ".join(desc)
378        return desc
379
380    def set_command(self, profile, command=''):
381        self._set_it(profile, 'command', command)
382
383    def set_description(self, profile, description=''):
384        self._set_it(profile, 'description', description)
385
386    def get_profile(self, profile_name):
387        return {'profile': profile_name,
388                'command': self.get_command(profile_name),
389                'description': self.get_description(profile_name)}
390
391
392class NmapOutputHighlight(object):
393    setts = ["bold", "italic", "underline", "text", "highlight", "regex"]
394
395    def save_changes(self):
396        config_parser.save_changes()
397
398    def __get_it(self, p_name):
399        property_name = "%s_highlight" % p_name
400
401        try:
402            return self.sanity_settings([
403                config_parser.get(
404                    property_name, prop, True) for prop in self.setts])
405        except Exception:
406            settings = []
407            prop_settings = self.default_highlights[p_name]
408            settings.append(prop_settings["bold"])
409            settings.append(prop_settings["italic"])
410            settings.append(prop_settings["underline"])
411            settings.append(prop_settings["text"])
412            settings.append(prop_settings["highlight"])
413            settings.append(prop_settings["regex"])
414
415            self.__set_it(p_name, settings)
416
417            return settings
418
419    def __set_it(self, property_name, settings):
420        property_name = "%s_highlight" % property_name
421        settings = self.sanity_settings(list(settings))
422
423        for pos in xrange(len(settings)):
424            config_parser.set(property_name, self.setts[pos], settings[pos])
425
426    def sanity_settings(self, settings):
427        """This method tries to convert insane settings to sanity ones ;-)
428        If user send a True, "True" or "true" value, for example, it tries to
429        convert then to the integer 1.
430        Same to False, "False", etc.
431
432        Sequence: [bold, italic, underline, text, highlight, regex]
433        """
434        # log.debug(">>> Sanitize %s" % str(settings))
435
436        settings[0] = self.boolean_sanity(settings[0])
437        settings[1] = self.boolean_sanity(settings[1])
438        settings[2] = self.boolean_sanity(settings[2])
439
440        tuple_regex = "[\(\[]\s?(\d+)\s?,\s?(\d+)\s?,\s?(\d+)\s?[\)\]]"
441        if isinstance(settings[3], basestring):
442            settings[3] = [
443                    int(t) for t in re.findall(tuple_regex, settings[3])[0]
444                    ]
445
446        if isinstance(settings[4], basestring):
447            settings[4] = [
448                    int(h) for h in re.findall(tuple_regex, settings[4])[0]
449                    ]
450
451        return settings
452
453    def boolean_sanity(self, attr):
454        if attr is True or attr == "True" or attr == "true" or attr == "1":
455            return 1
456        return 0
457
458    def get_date(self):
459        return self.__get_it("date")
460
461    def set_date(self, settings):
462        self.__set_it("date", settings)
463
464    def get_hostname(self):
465        return self.__get_it("hostname")
466
467    def set_hostname(self, settings):
468        self.__set_it("hostname", settings)
469
470    def get_ip(self):
471        return self.__get_it("ip")
472
473    def set_ip(self, settings):
474        self.__set_it("ip", settings)
475
476    def get_port_list(self):
477        return self.__get_it("port_list")
478
479    def set_port_list(self, settings):
480        self.__set_it("port_list", settings)
481
482    def get_open_port(self):
483        return self.__get_it("open_port")
484
485    def set_open_port(self, settings):
486        self.__set_it("open_port", settings)
487
488    def get_closed_port(self):
489        return self.__get_it("closed_port")
490
491    def set_closed_port(self, settings):
492        self.__set_it("closed_port", settings)
493
494    def get_filtered_port(self):
495        return self.__get_it("filtered_port")
496
497    def set_filtered_port(self, settings):
498        self.__set_it("filtered_port", settings)
499
500    def get_details(self):
501        return self.__get_it("details")
502
503    def set_details(self, settings):
504        self.__set_it("details", settings)
505
506    def get_enable(self):
507        enable = True
508        try:
509            enable = config_parser.get("output_highlight", "enable_highlight")
510        except NoSectionError:
511            config_parser.set(
512                    "output_highlight", "enable_highlight", str(True))
513
514        if enable == "False" or enable == "0" or enable == "":
515            return False
516        return True
517
518    def set_enable(self, enable):
519        if enable is False or enable == "0" or enable is None or enable == "":
520            config_parser.set(
521                    "output_highlight", "enable_highlight", str(False))
522        else:
523            config_parser.set(
524                    "output_highlight", "enable_highlight", str(True))
525
526    date = property(get_date, set_date)
527    hostname = property(get_hostname, set_hostname)
528    ip = property(get_ip, set_ip)
529    port_list = property(get_port_list, set_port_list)
530    open_port = property(get_open_port, set_open_port)
531    closed_port = property(get_closed_port, set_closed_port)
532    filtered_port = property(get_filtered_port, set_filtered_port)
533    details = property(get_details, set_details)
534    enable = property(get_enable, set_enable)
535
536    # These settings are made when there is nothing set yet. They set the
537    # "factory" default to highlight colors
538    default_highlights = {
539            "date": {
540                "bold": str(True),
541                "italic": str(False),
542                "underline": str(False),
543                "text": [0, 0, 0],
544                "highlight": [65535, 65535, 65535],
545                "regex": "\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}\s.{1,4}"},
546            "hostname": {
547                "bold": str(True),
548                "italic": str(True),
549                "underline": str(True),
550                "text": [0, 111, 65535],
551                "highlight": [65535, 65535, 65535],
552                "regex": "(\w{2,}://)*[\w-]{2,}\.[\w-]{2,}"
553                         "(\.[\w-]{2,})*(/[[\w-]{2,}]*)*"},
554            "ip": {
555                "bold": str(True),
556                "italic": str(False),
557                "underline": str(False),
558                "text": [0, 0, 0],
559                "highlight": [65535, 65535, 65535],
560                "regex": "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"},
561            "port_list": {
562                "bold": str(True),
563                "italic": str(False),
564                "underline": str(False),
565                "text": [0, 1272, 28362],
566                "highlight": [65535, 65535, 65535],
567                "regex": "PORT\s+STATE\s+SERVICE(\s+VERSION)?[^\n]*"},
568            "open_port": {
569                "bold": str(True),
570                "italic": str(False),
571                "underline": str(False),
572                "text": [0, 41036, 2396],
573                "highlight": [65535, 65535, 65535],
574                "regex": "\d{1,5}/.{1,5}\s+open\s+.*"},
575            "closed_port": {
576                "bold": str(False),
577                "italic": str(False),
578                "underline": str(False),
579                "text": [65535, 0, 0],
580                "highlight": [65535, 65535, 65535],
581                "regex": "\d{1,5}/.{1,5}\s+closed\s+.*"},
582            "filtered_port": {
583                "bold": str(False),
584                "italic": str(False),
585                "underline": str(False),
586                "text": [38502, 39119, 0],
587                "highlight": [65535, 65535, 65535],
588                "regex": "\d{1,5}/.{1,5}\s+filtered\s+.*"},
589            "details": {
590                "bold": str(True),
591                "italic": str(False),
592                "underline": str(True),
593                "text": [0, 0, 0],
594                "highlight": [65535, 65535, 65535],
595                "regex": "^(\w{2,}[\s]{,3}){,4}:"}
596            }
597
598
599# Retrieve details from zenmap.conf regarding paths subsection
600# (e.g. nmap_command_path) - jurand
601class PathsConfig(object):
602    section_name = "paths"
603
604    # This accounts for missing entries conf file.
605    # Defaults to "nmap" if these errors occur.
606    # NoOptionError, NoSectionError
607    def __get_it(self, p_name, default):
608        try:
609            return config_parser.get(self.section_name, p_name)
610        except (NoOptionError, NoSectionError):
611            log.debug(
612                    ">>> Using default \"%s\" for \"%s\"." % (default, p_name))
613            return default
614
615    def __set_it(self, property_name, settings):
616        config_parser.set(self.section_name, property_name, settings)
617
618    def get_nmap_command_path(self):
619        return self.__get_it("nmap_command_path", "nmap")
620
621    def set_nmap_command_path(self, settings):
622        self.__set_it("nmap_command_path", settings)
623
624    def get_ndiff_command_path(self):
625        return self.__get_it("ndiff_command_path", "ndiff")
626
627    def set_ndiff_command_path(self, settings):
628        self.__set_it("ndiff_command_path", settings)
629
630    nmap_command_path = property(get_nmap_command_path, set_nmap_command_path)
631    ndiff_command_path = property(
632            get_ndiff_command_path, set_ndiff_command_path)
633
634
635# Exceptions
636class ProfileNotFound:
637    def __init__(self, profile):
638        self.profile = profile
639
640    def __str__(self):
641        return "No profile named '" + self.profile + "' found!"
642
643
644class ProfileCouldNotBeSaved:
645    def __init__(self, profile):
646        self.profile = profile
647
648    def __str__(self):
649        return "Profile named '" + self.profile + "' could not be saved!"
650