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