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