1# -*- coding: utf-8 -*- 2# Copyright (C) 2014-2021 Greenbone Networks GmbH 3# 4# SPDX-License-Identifier: AGPL-3.0-or-later 5# 6# This program is free software: you can redistribute it and/or modify 7# it under the terms of the GNU Affero General Public License as 8# published by the Free Software Foundation, either version 3 of the 9# License, or (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU Affero General Public License for more details. 15# 16# You should have received a copy of the GNU Affero General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18 19 20# pylint: disable=too-many-lines 21 22""" Setup for the OSP OpenVAS Server. """ 23 24import logging 25import time 26import copy 27 28from typing import Optional, Dict, List, Tuple, Iterator 29from datetime import datetime 30 31from pathlib import Path 32from os import geteuid 33from lxml.etree import tostring, SubElement, Element 34 35import psutil 36 37from ospd.ospd import OSPDaemon 38from ospd.scan import ScanProgress, ScanStatus 39from ospd.server import BaseServer 40from ospd.main import main as daemon_main 41from ospd.cvss import CVSS 42from ospd.vtfilter import VtsFilter 43from ospd.resultlist import ResultList 44 45from ospd_openvas import __version__ 46from ospd_openvas.errors import OspdOpenvasError 47 48from ospd_openvas.nvticache import NVTICache 49from ospd_openvas.db import MainDB, BaseDB 50from ospd_openvas.lock import LockFile 51from ospd_openvas.preferencehandler import PreferenceHandler 52from ospd_openvas.openvas import Openvas 53from ospd_openvas.vthelper import VtHelper 54 55logger = logging.getLogger(__name__) 56 57 58OSPD_DESC = """ 59This scanner runs OpenVAS to scan the target hosts. 60 61OpenVAS (Open Vulnerability Assessment Scanner) is a powerful scanner 62for vulnerabilities in IT infrastrucutres. The capabilities include 63unauthenticated scanning as well as authenticated scanning for 64various types of systems and services. 65 66For more details about OpenVAS see: 67http://www.openvas.org/ 68 69The current version of ospd-openvas is a simple frame, which sends 70the server parameters to the Greenbone Vulnerability Manager daemon (GVMd) and 71checks the existence of OpenVAS binary. But it can not run scans yet. 72""" 73 74OSPD_PARAMS = { 75 'auto_enable_dependencies': { 76 'type': 'boolean', 77 'name': 'auto_enable_dependencies', 78 'default': 1, 79 'mandatory': 1, 80 'visible_for_client': True, 81 'description': 'Automatically enable the plugins that are depended on', 82 }, 83 'cgi_path': { 84 'type': 'string', 85 'name': 'cgi_path', 86 'default': '/cgi-bin:/scripts', 87 'mandatory': 1, 88 'visible_for_client': True, 89 'description': 'Look for default CGIs in /cgi-bin and /scripts', 90 }, 91 'checks_read_timeout': { 92 'type': 'integer', 93 'name': 'checks_read_timeout', 94 'default': 5, 95 'mandatory': 1, 96 'visible_for_client': True, 97 'description': ( 98 'Number of seconds that the security checks will ' 99 + 'wait for when doing a recv()' 100 ), 101 }, 102 'non_simult_ports': { 103 'type': 'string', 104 'name': 'non_simult_ports', 105 'default': '139, 445, 3389, Services/irc', 106 'mandatory': 1, 107 'visible_for_client': True, 108 'description': ( 109 'Prevent to make two connections on the same given ' 110 + 'ports at the same time.' 111 ), 112 }, 113 'open_sock_max_attempts': { 114 'type': 'integer', 115 'name': 'open_sock_max_attempts', 116 'default': 5, 117 'mandatory': 0, 118 'visible_for_client': True, 119 'description': ( 120 'Number of unsuccessful retries to open the socket ' 121 + 'before to set the port as closed.' 122 ), 123 }, 124 'timeout_retry': { 125 'type': 'integer', 126 'name': 'timeout_retry', 127 'default': 5, 128 'mandatory': 0, 129 'visible_for_client': True, 130 'description': ( 131 'Number of retries when a socket connection attempt ' + 'timesout.' 132 ), 133 }, 134 'optimize_test': { 135 'type': 'boolean', 136 'name': 'optimize_test', 137 'default': 1, 138 'mandatory': 0, 139 'visible_for_client': True, 140 'description': ( 141 'By default, optimize_test is enabled which means openvas does ' 142 + 'trust the remote host banners and is only launching plugins ' 143 + 'against the services they have been designed to check. ' 144 + 'For example it will check a web server claiming to be IIS only ' 145 + 'for IIS related flaws but will skip plugins testing for Apache ' 146 + 'flaws, and so on. This default behavior is used to optimize ' 147 + 'the scanning performance and to avoid false positives. ' 148 + 'If you are not sure that the banners of the remote host ' 149 + 'have been tampered with, you can disable this option.' 150 ), 151 }, 152 'plugins_timeout': { 153 'type': 'integer', 154 'name': 'plugins_timeout', 155 'default': 5, 156 'mandatory': 0, 157 'visible_for_client': True, 158 'description': 'This is the maximum lifetime, in seconds of a plugin.', 159 }, 160 'report_host_details': { 161 'type': 'boolean', 162 'name': 'report_host_details', 163 'default': 1, 164 'mandatory': 1, 165 'visible_for_client': True, 166 'description': '', 167 }, 168 'safe_checks': { 169 'type': 'boolean', 170 'name': 'safe_checks', 171 'default': 1, 172 'mandatory': 1, 173 'visible_for_client': True, 174 'description': ( 175 'Disable the plugins with potential to crash ' 176 + 'the remote services' 177 ), 178 }, 179 'scanner_plugins_timeout': { 180 'type': 'integer', 181 'name': 'scanner_plugins_timeout', 182 'default': 36000, 183 'mandatory': 1, 184 'visible_for_client': True, 185 'description': 'Like plugins_timeout, but for ACT_SCANNER plugins.', 186 }, 187 'time_between_request': { 188 'type': 'integer', 189 'name': 'time_between_request', 190 'default': 0, 191 'mandatory': 0, 192 'visible_for_client': True, 193 'description': ( 194 'Allow to set a wait time between two actions ' 195 + '(open, send, close).' 196 ), 197 }, 198 'unscanned_closed': { 199 'type': 'boolean', 200 'name': 'unscanned_closed', 201 'default': 1, 202 'mandatory': 1, 203 'visible_for_client': True, 204 'description': '', 205 }, 206 'unscanned_closed_udp': { 207 'type': 'boolean', 208 'name': 'unscanned_closed_udp', 209 'default': 1, 210 'mandatory': 1, 211 'visible_for_client': True, 212 'description': '', 213 }, 214 'expand_vhosts': { 215 'type': 'boolean', 216 'name': 'expand_vhosts', 217 'default': 1, 218 'mandatory': 0, 219 'visible_for_client': True, 220 'description': 'Whether to expand the target hosts ' 221 + 'list of vhosts with values gathered from sources ' 222 + 'such as reverse-lookup queries and VT checks ' 223 + 'for SSL/TLS certificates.', 224 }, 225 'test_empty_vhost': { 226 'type': 'boolean', 227 'name': 'test_empty_vhost', 228 'default': 0, 229 'mandatory': 0, 230 'visible_for_client': True, 231 'description': 'If set to yes, the scanner will ' 232 + 'also test the target by using empty vhost value ' 233 + 'in addition to the targets associated vhost values.', 234 }, 235 'max_hosts': { 236 'type': 'integer', 237 'name': 'max_hosts', 238 'default': 30, 239 'mandatory': 0, 240 'visible_for_client': False, 241 'description': ( 242 'The maximum number of hosts to test at the same time which ' 243 + 'should be given to the client (which can override it). ' 244 + 'This value must be computed given your bandwidth, ' 245 + 'the number of hosts you want to test, your amount of ' 246 + 'memory and the performance of your processor(s).' 247 ), 248 }, 249 'max_checks': { 250 'type': 'integer', 251 'name': 'max_checks', 252 'default': 10, 253 'mandatory': 0, 254 'visible_for_client': False, 255 'description': ( 256 'The number of plugins that will run against each host being ' 257 + 'tested. Note that the total number of process will be max ' 258 + 'checks x max_hosts so you need to find a balance between ' 259 + 'these two options. Note that launching too many plugins at ' 260 + 'the same time may disable the remote host, either temporarily ' 261 + '(ie: inetd closes its ports) or definitely (the remote host ' 262 + 'crash because it is asked to do too many things at the ' 263 + 'same time), so be careful.' 264 ), 265 }, 266 'port_range': { 267 'type': 'string', 268 'name': 'port_range', 269 'default': '', 270 'mandatory': 0, 271 'visible_for_client': False, 272 'description': ( 273 'This is the default range of ports that the scanner plugins will ' 274 + 'probe. The syntax of this option is flexible, it can be a ' 275 + 'single range ("1-1500"), several ports ("21,23,80"), several ' 276 + 'ranges of ports ("1-1500,32000-33000"). Note that you can ' 277 + 'specify UDP and TCP ports by prefixing each range by T or U. ' 278 + 'For instance, the following range will make openvas scan UDP ' 279 + 'ports 1 to 1024 and TCP ports 1 to 65535 : ' 280 + '"T:1-65535,U:1-1024".' 281 ), 282 }, 283 'test_alive_hosts_only': { 284 'type': 'boolean', 285 'name': 'test_alive_hosts_only', 286 'default': 0, 287 'mandatory': 0, 288 'visible_for_client': False, 289 'description': ( 290 'If this option is set, openvas will scan the target list for ' 291 + 'alive hosts in a separate process while only testing those ' 292 + 'hosts which are identified as alive. This boosts the scan ' 293 + 'speed of target ranges with a high amount of dead hosts ' 294 + 'significantly.' 295 ), 296 }, 297 'source_iface': { 298 'type': 'string', 299 'name': 'source_iface', 300 'default': '', 301 'mandatory': 0, 302 'visible_for_client': False, 303 'description': ( 304 'Name of the network interface that will be used as the source ' 305 + 'of connections established by openvas. The scan won\'t be ' 306 + 'launched if the value isn\'t authorized according to ' 307 + '(sys_)ifaces_allow / (sys_)ifaces_deny if present.' 308 ), 309 }, 310 'ifaces_allow': { 311 'type': 'string', 312 'name': 'ifaces_allow', 313 'default': '', 314 'mandatory': 0, 315 'visible_for_client': False, 316 'description': ( 317 'Comma-separated list of interfaces names that are authorized ' 318 + 'as source_iface values.' 319 ), 320 }, 321 'ifaces_deny': { 322 'type': 'string', 323 'name': 'ifaces_deny', 324 'default': '', 325 'mandatory': 0, 326 'visible_for_client': False, 327 'description': ( 328 'Comma-separated list of interfaces names that are not ' 329 + 'authorized as source_iface values.' 330 ), 331 }, 332 'hosts_allow': { 333 'type': 'string', 334 'name': 'hosts_allow', 335 'default': '', 336 'mandatory': 0, 337 'visible_for_client': False, 338 'description': ( 339 'Comma-separated list of the only targets that are authorized ' 340 + 'to be scanned. Supports the same syntax as the list targets. ' 341 + 'Both target hostnames and the address to which they resolve ' 342 + 'are checked. Hostnames in hosts_allow list are not resolved ' 343 + 'however.' 344 ), 345 }, 346 'hosts_deny': { 347 'type': 'string', 348 'name': 'hosts_deny', 349 'default': '', 350 'mandatory': 0, 351 'visible_for_client': False, 352 'description': ( 353 'Comma-separated list of targets that are not authorized to ' 354 + 'be scanned. Supports the same syntax as the list targets. ' 355 + 'Both target hostnames and the address to which they resolve ' 356 + 'are checked. Hostnames in hosts_deny list are not ' 357 + 'resolved however.' 358 ), 359 }, 360} 361 362VT_BASE_OID = "1.3.6.1.4.1.25623." 363 364 365def safe_int(value: str) -> Optional[int]: 366 """Convert a string into an integer and return None in case of errors 367 during conversion 368 """ 369 try: 370 return int(value) 371 except (ValueError, TypeError): 372 return None 373 374 375class OpenVasVtsFilter(VtsFilter): 376 377 """Methods to overwrite the ones in the original class.""" 378 379 def __init__(self, nvticache: NVTICache) -> None: 380 super().__init__() 381 382 self.nvti = nvticache 383 384 def format_vt_modification_time(self, value: str) -> str: 385 """Convert the string seconds since epoch into a 19 character 386 string representing YearMonthDayHourMinuteSecond, 387 e.g. 20190319122532. This always refers to UTC. 388 """ 389 390 return datetime.utcfromtimestamp(int(value)).strftime("%Y%m%d%H%M%S") 391 392 def get_filtered_vts_list(self, vts, vt_filter: str) -> Optional[List[str]]: 393 """Gets a collection of vulnerability test from the redis cache, 394 which match the filter. 395 396 Arguments: 397 vt_filter: Filter to apply to the vts collection. 398 vts: The complete vts collection. 399 400 Returns: 401 List with filtered vulnerability tests. The list can be empty. 402 None in case of filter parse failure. 403 """ 404 filters = self.parse_filters(vt_filter) 405 if not filters: 406 return None 407 408 if not self.nvti: 409 return None 410 411 vt_oid_list = [vtlist[1] for vtlist in self.nvti.get_oids()] 412 vt_oid_list_temp = copy.copy(vt_oid_list) 413 vthelper = VtHelper(self.nvti) 414 415 for element, oper, filter_val in filters: 416 for vt_oid in vt_oid_list_temp: 417 if vt_oid not in vt_oid_list: 418 continue 419 420 vt = vthelper.get_single_vt(vt_oid) 421 if vt is None or not vt.get(element): 422 vt_oid_list.remove(vt_oid) 423 continue 424 425 elem_val = vt.get(element) 426 val = self.format_filter_value(element, elem_val) 427 428 if self.filter_operator[oper](val, filter_val): 429 continue 430 else: 431 vt_oid_list.remove(vt_oid) 432 433 return vt_oid_list 434 435 436class OSPDopenvas(OSPDaemon): 437 438 """Class for ospd-openvas daemon.""" 439 440 def __init__( 441 self, *, niceness=None, lock_file_dir='/var/lib/openvas', **kwargs 442 ): 443 """Initializes the ospd-openvas daemon's internal data.""" 444 self.main_db = MainDB() 445 self.nvti = NVTICache(self.main_db) 446 447 super().__init__( 448 customvtfilter=OpenVasVtsFilter(self.nvti), 449 storage=dict, 450 file_storage_dir=lock_file_dir, 451 **kwargs, 452 ) 453 454 self.server_version = __version__ 455 456 self._niceness = str(niceness) 457 458 self.feed_lock = LockFile(Path(lock_file_dir) / 'feed-update.lock') 459 self.daemon_info['name'] = 'OSPd OpenVAS' 460 self.scanner_info['name'] = 'openvas' 461 self.scanner_info['version'] = '' # achieved during self.init() 462 self.scanner_info['description'] = OSPD_DESC 463 464 for name, param in OSPD_PARAMS.items(): 465 self.set_scanner_param(name, param) 466 467 self._sudo_available = None 468 self._is_running_as_root = None 469 470 self.scan_only_params = dict() 471 472 def init(self, server: BaseServer) -> None: 473 474 self.scan_collection.init() 475 476 server.start(self.handle_client_stream) 477 478 self.scanner_info['version'] = Openvas.get_version() 479 480 self.set_params_from_openvas_settings() 481 482 with self.feed_lock.wait_for_lock(): 483 Openvas.load_vts_into_redis() 484 current_feed = self.nvti.get_feed_version() 485 self.set_vts_version(vts_version=current_feed) 486 487 logger.debug("Calculating vts integrity check hash...") 488 vthelper = VtHelper(self.nvti) 489 self.vts.sha256_hash = vthelper.calculate_vts_collection_hash() 490 491 self.initialized = True 492 493 def set_params_from_openvas_settings(self): 494 """Set OSPD_PARAMS with the params taken from the openvas executable.""" 495 param_list = Openvas.get_settings() 496 497 for elem in param_list: # pylint: disable=consider-using-dict-items 498 if elem not in OSPD_PARAMS: 499 self.scan_only_params[elem] = param_list[elem] 500 else: 501 OSPD_PARAMS[elem]['default'] = param_list[elem] 502 503 def feed_is_outdated(self, current_feed: str) -> Optional[bool]: 504 """Compare the current feed with the one in the disk. 505 506 Return: 507 False if there is no new feed. 508 True if the feed version in disk is newer than the feed in 509 redis cache. 510 None if there is no feed on the disk. 511 """ 512 plugins_folder = self.scan_only_params.get('plugins_folder') 513 if not plugins_folder: 514 raise OspdOpenvasError("Error: Path to plugins folder not found.") 515 516 feed_info_file = Path(plugins_folder) / 'plugin_feed_info.inc' 517 if not feed_info_file.exists(): 518 self.set_params_from_openvas_settings() 519 logger.debug('Plugins feed file %s not found.', feed_info_file) 520 return None 521 522 current_feed = safe_int(current_feed) 523 if current_feed is None: 524 logger.debug( 525 "Wrong PLUGIN_SET format in plugins feed file %s. Format has to" 526 " be yyyymmddhhmm. For example 'PLUGIN_SET = \"201910251033\"'", 527 feed_info_file, 528 ) 529 530 feed_date = None 531 with feed_info_file.open() as fcontent: 532 for line in fcontent: 533 if "PLUGIN_SET" in line: 534 feed_date = line.split('=', 1)[1] 535 feed_date = feed_date.strip() 536 feed_date = feed_date.replace(';', '') 537 feed_date = feed_date.replace('"', '') 538 feed_date = safe_int(feed_date) 539 break 540 541 logger.debug("Current feed version: %s", current_feed) 542 logger.debug("Plugin feed version: %s", feed_date) 543 544 return ( 545 (not feed_date) or (not current_feed) or (current_feed < feed_date) 546 ) 547 548 def check_feed(self): 549 """Check if there is a feed update. 550 551 Wait until all the running scans finished. Set a flag to announce there 552 is a pending feed update, which avoids to start a new scan. 553 """ 554 if not self.vts.is_cache_available: 555 return 556 557 current_feed = self.nvti.get_feed_version() 558 is_outdated = self.feed_is_outdated(current_feed) 559 560 # Check if the nvticache in redis is outdated 561 if not current_feed or is_outdated: 562 with self.feed_lock as fl: 563 if fl.has_lock(): 564 self.initialized = False 565 Openvas.load_vts_into_redis() 566 current_feed = self.nvti.get_feed_version() 567 self.set_vts_version(vts_version=current_feed) 568 569 vthelper = VtHelper(self.nvti) 570 self.vts.sha256_hash = ( 571 vthelper.calculate_vts_collection_hash() 572 ) 573 self.initialized = True 574 else: 575 logger.debug( 576 "The feed was not upload or it is outdated, " 577 "but other process is locking the update. " 578 "Trying again later..." 579 ) 580 return 581 582 def scheduler(self): 583 """This method is called periodically to run tasks.""" 584 self.check_feed() 585 586 def get_vt_iterator( 587 self, vt_selection: List[str] = None, details: bool = True 588 ) -> Iterator[Tuple[str, Dict]]: 589 vthelper = VtHelper(self.nvti) 590 return vthelper.get_vt_iterator(vt_selection, details) 591 592 @staticmethod 593 def get_custom_vt_as_xml_str(vt_id: str, custom: Dict) -> str: 594 """Return an xml element with custom metadata formatted as string. 595 Arguments: 596 vt_id: VT OID. Only used for logging in error case. 597 custom: Dictionary with the custom metadata. 598 Return: 599 Xml element as string. 600 """ 601 602 _custom = Element('custom') 603 for key, val in custom.items(): 604 xml_key = SubElement(_custom, key) 605 try: 606 xml_key.text = val 607 except ValueError as e: 608 logger.warning( 609 "Not possible to parse custom tag for VT %s: %s", vt_id, e 610 ) 611 return tostring(_custom).decode('utf-8') 612 613 @staticmethod 614 def get_severities_vt_as_xml_str(vt_id: str, severities: Dict) -> str: 615 """Return an xml element with severities as string. 616 Arguments: 617 vt_id: VT OID. Only used for logging in error case. 618 severities: Dictionary with the severities. 619 Return: 620 Xml element as string. 621 """ 622 _severities = Element('severities') 623 _severity = SubElement(_severities, 'severity') 624 if 'severity_base_vector' in severities: 625 try: 626 _value = SubElement(_severity, 'value') 627 _value.text = severities.get('severity_base_vector') 628 except ValueError as e: 629 logger.warning( 630 "Not possible to parse severity tag for vt %s: %s", vt_id, e 631 ) 632 if 'severity_origin' in severities: 633 _origin = SubElement(_severity, 'origin') 634 _origin.text = severities.get('severity_origin') 635 if 'severity_date' in severities: 636 _date = SubElement(_severity, 'date') 637 _date.text = severities.get('severity_date') 638 if 'severity_type' in severities: 639 _severity.set('type', severities.get('severity_type')) 640 641 return tostring(_severities).decode('utf-8') 642 643 @staticmethod 644 def get_params_vt_as_xml_str(vt_id: str, vt_params: Dict) -> str: 645 """Return an xml element with params formatted as string. 646 Arguments: 647 vt_id: VT OID. Only used for logging in error case. 648 vt_params: Dictionary with the VT parameters. 649 Return: 650 Xml element as string. 651 """ 652 vt_params_xml = Element('params') 653 for _pref_id, prefs in vt_params.items(): 654 vt_param = Element('param') 655 vt_param.set('type', prefs['type']) 656 vt_param.set('id', _pref_id) 657 xml_name = SubElement(vt_param, 'name') 658 try: 659 xml_name.text = prefs['name'] 660 except ValueError as e: 661 logger.warning( 662 "Not possible to parse parameter for VT %s: %s", vt_id, e 663 ) 664 if prefs['default']: 665 xml_def = SubElement(vt_param, 'default') 666 try: 667 xml_def.text = prefs['default'] 668 except ValueError as e: 669 logger.warning( 670 "Not possible to parse default parameter for VT %s: %s", 671 vt_id, 672 e, 673 ) 674 vt_params_xml.append(vt_param) 675 676 return tostring(vt_params_xml).decode('utf-8') 677 678 @staticmethod 679 def get_refs_vt_as_xml_str(vt_id: str, vt_refs: Dict) -> str: 680 """Return an xml element with references formatted as string. 681 Arguments: 682 vt_id: VT OID. Only used for logging in error case. 683 vt_refs: Dictionary with the VT references. 684 Return: 685 Xml element as string. 686 """ 687 vt_refs_xml = Element('refs') 688 for ref_type, ref_values in vt_refs.items(): 689 for value in ref_values: 690 vt_ref = Element('ref') 691 if ref_type == "xref" and value: 692 for xref in value.split(', '): 693 try: 694 _type, _id = xref.split(':', 1) 695 except ValueError as e: 696 logger.error( 697 'Not possible to parse xref "%s" for VT %s: %s', 698 xref, 699 vt_id, 700 e, 701 ) 702 continue 703 vt_ref.set('type', _type.lower()) 704 vt_ref.set('id', _id) 705 elif value: 706 vt_ref.set('type', ref_type.lower()) 707 vt_ref.set('id', value) 708 else: 709 continue 710 vt_refs_xml.append(vt_ref) 711 712 return tostring(vt_refs_xml).decode('utf-8') 713 714 @staticmethod 715 def get_dependencies_vt_as_xml_str( 716 vt_id: str, vt_dependencies: List 717 ) -> str: 718 """Return an xml element with dependencies as string. 719 Arguments: 720 vt_id: VT OID. Only used for logging in error case. 721 vt_dependencies: List with the VT dependencies. 722 Return: 723 Xml element as string. 724 """ 725 vt_deps_xml = Element('dependencies') 726 for dep in vt_dependencies: 727 _vt_dep = Element('dependency') 728 if VT_BASE_OID in dep: 729 _vt_dep.set('vt_id', dep) 730 else: 731 logger.error( 732 'Not possible to add dependency %s for VT %s', dep, vt_id 733 ) 734 continue 735 vt_deps_xml.append(_vt_dep) 736 737 return tostring(vt_deps_xml).decode('utf-8') 738 739 @staticmethod 740 def get_creation_time_vt_as_xml_str( 741 vt_id: str, vt_creation_time: str 742 ) -> str: 743 """Return creation time as string. 744 Arguments: 745 vt_id: VT OID. Only used for logging in error case. 746 vt_creation_time: String with the VT creation time. 747 Return: 748 Xml element as string. 749 """ 750 _time = Element('creation_time') 751 try: 752 _time.text = vt_creation_time 753 except ValueError as e: 754 logger.warning( 755 "Not possible to parse creation time for VT %s: %s", vt_id, e 756 ) 757 return tostring(_time).decode('utf-8') 758 759 @staticmethod 760 def get_modification_time_vt_as_xml_str( 761 vt_id: str, vt_modification_time: str 762 ) -> str: 763 """Return modification time as string. 764 Arguments: 765 vt_id: VT OID. Only used for logging in error case. 766 vt_modification_time: String with the VT modification time. 767 Return: 768 Xml element as string. 769 """ 770 _time = Element('modification_time') 771 try: 772 _time.text = vt_modification_time 773 except ValueError as e: 774 logger.warning( 775 "Not possible to parse modification time for VT %s: %s", 776 vt_id, 777 e, 778 ) 779 return tostring(_time).decode('utf-8') 780 781 @staticmethod 782 def get_summary_vt_as_xml_str(vt_id: str, summary: str) -> str: 783 """Return summary as string. 784 Arguments: 785 vt_id: VT OID. Only used for logging in error case. 786 summary: String with a VT summary. 787 Return: 788 Xml element as string. 789 """ 790 _summary = Element('summary') 791 try: 792 _summary.text = summary 793 except ValueError as e: 794 logger.warning( 795 "Not possible to parse summary tag for VT %s: %s", vt_id, e 796 ) 797 return tostring(_summary).decode('utf-8') 798 799 @staticmethod 800 def get_impact_vt_as_xml_str(vt_id: str, impact) -> str: 801 """Return impact as string. 802 803 Arguments: 804 vt_id (str): VT OID. Only used for logging in error case. 805 impact (str): String which explain the vulneravility impact. 806 Return: 807 string: xml element as string. 808 """ 809 _impact = Element('impact') 810 try: 811 _impact.text = impact 812 except ValueError as e: 813 logger.warning( 814 "Not possible to parse impact tag for VT %s: %s", vt_id, e 815 ) 816 return tostring(_impact).decode('utf-8') 817 818 @staticmethod 819 def get_affected_vt_as_xml_str(vt_id: str, affected: str) -> str: 820 """Return affected as string. 821 Arguments: 822 vt_id: VT OID. Only used for logging in error case. 823 affected: String which explain what is affected. 824 Return: 825 Xml element as string. 826 """ 827 _affected = Element('affected') 828 try: 829 _affected.text = affected 830 except ValueError as e: 831 logger.warning( 832 "Not possible to parse affected tag for VT %s: %s", vt_id, e 833 ) 834 return tostring(_affected).decode('utf-8') 835 836 @staticmethod 837 def get_insight_vt_as_xml_str(vt_id: str, insight: str) -> str: 838 """Return insight as string. 839 Arguments: 840 vt_id: VT OID. Only used for logging in error case. 841 insight: String giving an insight of the vulnerability. 842 Return: 843 Xml element as string. 844 """ 845 _insight = Element('insight') 846 try: 847 _insight.text = insight 848 except ValueError as e: 849 logger.warning( 850 "Not possible to parse insight tag for VT %s: %s", vt_id, e 851 ) 852 return tostring(_insight).decode('utf-8') 853 854 @staticmethod 855 def get_solution_vt_as_xml_str( 856 vt_id: str, 857 solution: str, 858 solution_type: Optional[str] = None, 859 solution_method: Optional[str] = None, 860 ) -> str: 861 """Return solution as string. 862 Arguments: 863 vt_id: VT OID. Only used for logging in error case. 864 solution: String giving a possible solution. 865 solution_type: A solution type 866 solution_method: A solution method 867 Return: 868 Xml element as string. 869 """ 870 _solution = Element('solution') 871 try: 872 _solution.text = solution 873 except ValueError as e: 874 logger.warning( 875 "Not possible to parse solution tag for VT %s: %s", vt_id, e 876 ) 877 if solution_type: 878 _solution.set('type', solution_type) 879 if solution_method: 880 _solution.set('method', solution_method) 881 return tostring(_solution).decode('utf-8') 882 883 @staticmethod 884 def get_detection_vt_as_xml_str( 885 vt_id: str, 886 detection: Optional[str] = None, 887 qod_type: Optional[str] = None, 888 qod: Optional[str] = None, 889 ) -> str: 890 """Return detection as string. 891 Arguments: 892 vt_id: VT OID. Only used for logging in error case. 893 detection: String which explain how the vulnerability 894 was detected. 895 qod_type: qod type. 896 qod: qod value. 897 Return: 898 Xml element as string. 899 """ 900 _detection = Element('detection') 901 if detection: 902 try: 903 _detection.text = detection 904 except ValueError as e: 905 logger.warning( 906 "Not possible to parse detection tag for VT %s: %s", 907 vt_id, 908 e, 909 ) 910 if qod_type: 911 _detection.set('qod_type', qod_type) 912 elif qod: 913 _detection.set('qod', qod) 914 915 return tostring(_detection).decode('utf-8') 916 917 @property 918 def is_running_as_root(self) -> bool: 919 """Check if it is running as root user.""" 920 if self._is_running_as_root is not None: 921 return self._is_running_as_root 922 923 self._is_running_as_root = False 924 if geteuid() == 0: 925 self._is_running_as_root = True 926 927 return self._is_running_as_root 928 929 @property 930 def sudo_available(self) -> bool: 931 """Checks that sudo is available""" 932 if self._sudo_available is not None: 933 return self._sudo_available 934 935 if self.is_running_as_root: 936 self._sudo_available = False 937 return self._sudo_available 938 939 self._sudo_available = Openvas.check_sudo() 940 941 return self._sudo_available 942 943 def check(self) -> bool: 944 """Checks that openvas command line tool is found and 945 is executable.""" 946 has_openvas = Openvas.check() 947 if not has_openvas: 948 logger.error( 949 'openvas executable not available. Please install openvas' 950 ' into your PATH.' 951 ) 952 return has_openvas 953 954 def report_openvas_scan_status(self, kbdb: BaseDB, scan_id: str): 955 """Get all status entries from redis kb. 956 957 Arguments: 958 kbdb: KB context where to get the status from. 959 scan_id: Scan ID to identify the current scan. 960 """ 961 all_status = kbdb.get_scan_status() 962 all_hosts = dict() 963 finished_hosts = list() 964 for res in all_status: 965 try: 966 current_host, launched, total = res.split('/') 967 except ValueError: 968 continue 969 970 try: 971 if float(total) == 0: 972 continue 973 elif float(total) == ScanProgress.DEAD_HOST: 974 host_prog = ScanProgress.DEAD_HOST 975 else: 976 host_prog = int((float(launched) / float(total)) * 100) 977 except TypeError: 978 continue 979 980 all_hosts[current_host] = host_prog 981 982 if ( 983 host_prog == ScanProgress.DEAD_HOST 984 or host_prog == ScanProgress.FINISHED 985 ): 986 finished_hosts.append(current_host) 987 988 logger.debug( 989 '%s: Host %s has progress: %d', scan_id, current_host, host_prog 990 ) 991 992 self.set_scan_progress_batch(scan_id, host_progress=all_hosts) 993 994 self.sort_host_finished(scan_id, finished_hosts) 995 996 def get_severity_score(self, vt_aux: dict) -> Optional[float]: 997 """Return the severity score for the given oid. 998 Arguments: 999 vt_aux: VT element from which to get the severity vector 1000 Returns: 1001 The calculated cvss base value. None if there is no severity 1002 vector or severity type is not cvss base version 2. 1003 """ 1004 if vt_aux: 1005 severity_type = vt_aux['severities'].get('severity_type') 1006 severity_vector = vt_aux['severities'].get('severity_base_vector') 1007 1008 if severity_type == "cvss_base_v2" and severity_vector: 1009 return CVSS.cvss_base_v2_value(severity_vector) 1010 elif severity_type == "cvss_base_v3" and severity_vector: 1011 return CVSS.cvss_base_v3_value(severity_vector) 1012 1013 return None 1014 1015 def report_openvas_results(self, db: BaseDB, scan_id: str) -> bool: 1016 """Get all result entries from redis kb.""" 1017 1018 vthelper = VtHelper(self.nvti) 1019 1020 # Result messages come in the next form, with optional uri field 1021 # type ||| host ip ||| hostname ||| port ||| OID ||| value [|||uri] 1022 all_results = db.get_result() 1023 res_list = ResultList() 1024 total_dead = 0 1025 for res in all_results: 1026 if not res: 1027 continue 1028 1029 msg = res.split('|||') 1030 roid = msg[4].strip() 1031 rqod = '' 1032 rname = '' 1033 current_host = msg[1].strip() if msg[1] else '' 1034 rhostname = msg[2].strip() if msg[2] else '' 1035 host_is_dead = "Host dead" in msg[5] or msg[0] == "DEADHOST" 1036 host_deny = "Host access denied" in msg[5] 1037 start_end_msg = msg[0] == "HOST_START" or msg[0] == "HOST_END" 1038 host_count = msg[0] == "HOSTS_COUNT" 1039 vt_aux = None 1040 1041 # URI is optional and msg list length must be checked 1042 ruri = '' 1043 if len(msg) > 6: 1044 ruri = msg[6] 1045 1046 if ( 1047 roid 1048 and not host_is_dead 1049 and not host_deny 1050 and not start_end_msg 1051 and not host_count 1052 ): 1053 vt_aux = vthelper.get_single_vt(roid) 1054 1055 if ( 1056 not vt_aux 1057 and not host_is_dead 1058 and not host_deny 1059 and not start_end_msg 1060 and not host_count 1061 ): 1062 logger.warning('Invalid VT oid %s for a result', roid) 1063 1064 if vt_aux: 1065 if vt_aux.get('qod_type'): 1066 qod_t = vt_aux.get('qod_type') 1067 rqod = self.nvti.QOD_TYPES[qod_t] 1068 elif vt_aux.get('qod'): 1069 rqod = vt_aux.get('qod') 1070 1071 rname = vt_aux.get('name') 1072 1073 if msg[0] == 'ERRMSG': 1074 res_list.add_scan_error_to_list( 1075 host=current_host, 1076 hostname=rhostname, 1077 name=rname, 1078 value=msg[5], 1079 port=msg[3], 1080 test_id=roid, 1081 uri=ruri, 1082 ) 1083 1084 elif msg[0] == 'HOST_START' or msg[0] == 'HOST_END': 1085 res_list.add_scan_log_to_list( 1086 host=current_host, 1087 name=msg[0], 1088 value=msg[5], 1089 ) 1090 1091 elif msg[0] == 'LOG': 1092 res_list.add_scan_log_to_list( 1093 host=current_host, 1094 hostname=rhostname, 1095 name=rname, 1096 value=msg[5], 1097 port=msg[3], 1098 qod=rqod, 1099 test_id=roid, 1100 uri=ruri, 1101 ) 1102 1103 elif msg[0] == 'HOST_DETAIL': 1104 res_list.add_scan_host_detail_to_list( 1105 host=current_host, 1106 hostname=rhostname, 1107 name=rname, 1108 value=msg[5], 1109 uri=ruri, 1110 ) 1111 1112 elif msg[0] == 'ALARM': 1113 rseverity = self.get_severity_score(vt_aux) 1114 res_list.add_scan_alarm_to_list( 1115 host=current_host, 1116 hostname=rhostname, 1117 name=rname, 1118 value=msg[5], 1119 port=msg[3], 1120 test_id=roid, 1121 severity=rseverity, 1122 qod=rqod, 1123 uri=ruri, 1124 ) 1125 1126 # To process non-scanned dead hosts when 1127 # test_alive_host_only in openvas is enable 1128 elif msg[0] == 'DEADHOST': 1129 try: 1130 total_dead = int(msg[5]) 1131 except TypeError: 1132 logger.debug('Error processing dead host count') 1133 1134 # To update total host count 1135 if msg[0] == 'HOSTS_COUNT': 1136 try: 1137 count_total = int(msg[5]) 1138 logger.debug( 1139 '%s: Set total hosts counted by OpenVAS: %d', 1140 scan_id, 1141 count_total, 1142 ) 1143 self.set_scan_total_hosts(scan_id, count_total) 1144 except TypeError: 1145 logger.debug('Error processing total host count') 1146 1147 # Insert result batch into the scan collection table. 1148 if len(res_list): 1149 self.scan_collection.add_result_list(scan_id, res_list) 1150 logger.debug( 1151 '%s: Inserting %d results into scan collection table', 1152 scan_id, 1153 len(res_list), 1154 ) 1155 if total_dead: 1156 logger.debug( 1157 '%s: Set dead hosts counted by OpenVAS: %d', 1158 scan_id, 1159 total_dead, 1160 ) 1161 self.scan_collection.set_amount_dead_hosts( 1162 scan_id, total_dead=total_dead 1163 ) 1164 1165 return len(res_list) > 0 1166 1167 @staticmethod 1168 def is_openvas_process_alive(openvas_process: psutil.Popen) -> bool: 1169 1170 if openvas_process.status() == psutil.STATUS_ZOMBIE: 1171 logger.debug("Process is a Zombie, waiting for it to clean up") 1172 openvas_process.wait() 1173 return openvas_process.is_running() 1174 1175 def stop_scan_cleanup( 1176 self, 1177 kbdb: BaseDB, 1178 scan_id: str, 1179 ovas_process: psutil.Popen, # pylint: disable=arguments-differ 1180 ): 1181 """Set a key in redis to indicate the wrapper is stopped. 1182 It is done through redis because it is a new multiprocess 1183 instance and it is not possible to reach the variables 1184 of the grandchild process. 1185 Indirectly sends SIGUSR1 to the running openvas scan process 1186 via an invocation of openvas with the --scan-stop option to 1187 stop it.""" 1188 1189 if kbdb: 1190 # Set stop flag in redis 1191 kbdb.stop_scan(scan_id) 1192 1193 # Check if openvas is running 1194 if ovas_process.is_running(): 1195 # Cleaning in case of Zombie Process 1196 if ovas_process.status() == psutil.STATUS_ZOMBIE: 1197 logger.debug( 1198 '%s: Process with PID %s is a Zombie process.' 1199 ' Cleaning up...', 1200 scan_id, 1201 ovas_process.pid, 1202 ) 1203 ovas_process.wait() 1204 # Stop openvas process and wait until it stopped 1205 else: 1206 can_stop_scan = Openvas.stop_scan( 1207 scan_id, 1208 not self.is_running_as_root and self.sudo_available, 1209 ) 1210 if not can_stop_scan: 1211 logger.debug( 1212 'Not possible to stop scan process: %s.', 1213 ovas_process, 1214 ) 1215 return 1216 1217 logger.debug('Stopping process: %s', ovas_process) 1218 1219 while ovas_process.is_running(): 1220 if ovas_process.status() == psutil.STATUS_ZOMBIE: 1221 ovas_process.wait() 1222 else: 1223 time.sleep(0.1) 1224 else: 1225 logger.debug( 1226 "%s: Process with PID %s already stopped", 1227 scan_id, 1228 ovas_process.pid, 1229 ) 1230 1231 # Clean redis db 1232 for scan_db in kbdb.get_scan_databases(): 1233 self.main_db.release_database(scan_db) 1234 1235 def exec_scan(self, scan_id: str): 1236 """Starts the OpenVAS scanner for scan_id scan.""" 1237 do_not_launch = False 1238 kbdb = self.main_db.get_new_kb_database() 1239 scan_prefs = PreferenceHandler( 1240 scan_id, kbdb, self.scan_collection, self.nvti 1241 ) 1242 kbdb.add_scan_id(scan_id) 1243 scan_prefs.prepare_target_for_openvas() 1244 1245 if not scan_prefs.prepare_ports_for_openvas(): 1246 self.add_scan_error( 1247 scan_id, name='', host='', value='No port list defined.' 1248 ) 1249 do_not_launch = True 1250 1251 # Set credentials 1252 if not scan_prefs.prepare_credentials_for_openvas(): 1253 self.add_scan_error( 1254 scan_id, name='', host='', value='Malformed credential.' 1255 ) 1256 do_not_launch = True 1257 1258 if not scan_prefs.prepare_plugins_for_openvas(): 1259 self.add_scan_error( 1260 scan_id, name='', host='', value='No VTS to run.' 1261 ) 1262 do_not_launch = True 1263 1264 scan_prefs.prepare_main_kbindex_for_openvas() 1265 scan_prefs.prepare_host_options_for_openvas() 1266 scan_prefs.prepare_scan_params_for_openvas(OSPD_PARAMS) 1267 scan_prefs.prepare_reverse_lookup_opt_for_openvas() 1268 scan_prefs.prepare_alive_test_option_for_openvas() 1269 1270 # VT preferences are stored after all preferences have been processed, 1271 # since alive tests preferences have to be able to overwrite default 1272 # preferences of ping_host.nasl for the classic method. 1273 scan_prefs.prepare_nvt_preferences() 1274 scan_prefs.prepare_boreas_alive_test() 1275 1276 # Release memory used for scan preferences. 1277 del scan_prefs 1278 1279 if do_not_launch or kbdb.scan_is_stopped(scan_id): 1280 self.main_db.release_database(kbdb) 1281 return 1282 1283 openvas_process = Openvas.start_scan( 1284 scan_id, 1285 not self.is_running_as_root and self.sudo_available, 1286 self._niceness, 1287 ) 1288 1289 if openvas_process is None: 1290 self.main_db.release_database(kbdb) 1291 return 1292 1293 kbdb.add_scan_process_id(openvas_process.pid) 1294 logger.debug('pid = %s', openvas_process.pid) 1295 1296 # Wait until the scanner starts and loads all the preferences. 1297 while kbdb.get_status(scan_id) == 'new': 1298 res = openvas_process.poll() 1299 if res and res < 0: 1300 self.stop_scan_cleanup(kbdb, scan_id, openvas_process) 1301 logger.error( 1302 'It was not possible run the task %s, since openvas ended ' 1303 'unexpectedly with errors during launching.', 1304 scan_id, 1305 ) 1306 return 1307 1308 time.sleep(1) 1309 1310 got_results = False 1311 while True: 1312 1313 openvas_process_is_alive = self.is_openvas_process_alive( 1314 openvas_process 1315 ) 1316 target_is_finished = kbdb.target_is_finished(scan_id) 1317 scan_stopped = self.get_scan_status(scan_id) == ScanStatus.STOPPED 1318 1319 # Report new Results and update status 1320 got_results = self.report_openvas_results(kbdb, scan_id) 1321 self.report_openvas_scan_status(kbdb, scan_id) 1322 1323 # Check if the client stopped the whole scan 1324 if scan_stopped: 1325 logger.debug('%s: Scan stopped by the client', scan_id) 1326 1327 self.stop_scan_cleanup(kbdb, scan_id, openvas_process) 1328 1329 # clean main_db, but wait for scanner to finish. 1330 while not kbdb.target_is_finished(scan_id): 1331 logger.debug('%s: Waiting for openvas to finish', scan_id) 1332 time.sleep(1) 1333 self.main_db.release_database(kbdb) 1334 return 1335 1336 # Scan end. No kb in use for this scan id 1337 if target_is_finished: 1338 logger.debug('%s: Target is finished', scan_id) 1339 break 1340 1341 if not openvas_process_is_alive: 1342 logger.error( 1343 'Task %s was unexpectedly stopped or killed.', 1344 scan_id, 1345 ) 1346 self.add_scan_error( 1347 scan_id, 1348 name='', 1349 host='', 1350 value='Task was unexpectedly stopped or killed.', 1351 ) 1352 1353 # check for scanner error messages before leaving. 1354 self.report_openvas_results(kbdb, scan_id) 1355 1356 kbdb.stop_scan(scan_id) 1357 1358 for scan_db in kbdb.get_scan_databases(): 1359 self.main_db.release_database(scan_db) 1360 self.main_db.release_database(kbdb) 1361 return 1362 1363 # Wait a second before trying to get result from redis if there 1364 # was no results before. 1365 # Otherwise, wait 50 msec to give access other process to redis. 1366 if not got_results: 1367 time.sleep(1) 1368 else: 1369 time.sleep(0.05) 1370 got_results = False 1371 1372 # Delete keys from KB related to this scan task. 1373 logger.debug('%s: End Target. Release main database', scan_id) 1374 self.main_db.release_database(kbdb) 1375 1376 1377def main(): 1378 """OSP openvas main function.""" 1379 daemon_main('OSPD - openvas', OSPDopenvas) 1380 1381 1382if __name__ == '__main__': 1383 main() 1384