Passed
Pull Request — master (#424)
by Juan José
01:36
created

OSPDopenvas.is_running_as_root()   A

Complexity

Conditions 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
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
24
import logging
25
import time
26
import copy
27
28
from typing import Optional, Dict, List, Tuple, Iterator
29
from datetime import datetime
30
31
from pathlib import Path
32
from os import geteuid
33
from lxml.etree import tostring, SubElement, Element
34
35
import psutil
36
37
from ospd.ospd import OSPDaemon
38
from ospd.scan import ScanProgress
39
from ospd.server import BaseServer
40
from ospd.main import main as daemon_main
41
from ospd.vtfilter import VtsFilter
42
from ospd.resultlist import ResultList
43
44
from ospd_openvas import __version__
45
from ospd_openvas.errors import OspdOpenvasError
46
47
from ospd_openvas.dryrun import DryRun
48
from ospd_openvas.nvticache import NVTICache
49
from ospd_openvas.db import MainDB, BaseDB
50
from ospd_openvas.lock import LockFile
51
from ospd_openvas.preferencehandler import PreferenceHandler
52
from ospd_openvas.openvas import Openvas
53
from ospd_openvas.vthelper import VtHelper
54
55
logger = logging.getLogger(__name__)
56
57
58
OSPD_DESC = """
59
This scanner runs OpenVAS to scan the target hosts.
60
61
OpenVAS (Open Vulnerability Assessment Scanner) is a powerful scanner
62
for vulnerabilities in IT infrastrucutres. The capabilities include
63
unauthenticated scanning as well as authenticated scanning for
64
various types of systems and services.
65
66
For more details about OpenVAS see:
67
http://www.openvas.org/
68
69
The current version of ospd-openvas is a simple frame, which sends
70
the server parameters to the Greenbone Vulnerability Manager daemon (GVMd) and
71
checks the existence of OpenVAS binary. But it can not run scans yet.
72
"""
73
74
OSPD_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
    'hosts_allow': {
298
        'type': 'string',
299
        'name': 'hosts_allow',
300
        'default': '',
301
        'mandatory': 0,
302
        'visible_for_client': False,
303
        'description': (
304
            'Comma-separated list of the only targets that are authorized '
305
            + 'to be scanned. Supports the same syntax as the list targets. '
306
            + 'Both target hostnames and the address to which they resolve '
307
            + 'are checked. Hostnames in hosts_allow list are not resolved '
308
            + 'however.'
309
        ),
310
    },
311
    'hosts_deny': {
312
        'type': 'string',
313
        'name': 'hosts_deny',
314
        'default': '',
315
        'mandatory': 0,
316
        'visible_for_client': False,
317
        'description': (
318
            'Comma-separated list of targets that are not authorized to '
319
            + 'be scanned. Supports the same syntax as the list targets. '
320
            + 'Both target hostnames and the address to which they resolve '
321
            + 'are checked. Hostnames in hosts_deny list are not '
322
            + 'resolved however.'
323
        ),
324
    },
325
    'results_per_host': {
326
        'type': 'integer',
327
        'name': 'results_per_host',
328
        'default': 10,
329
        'mandatory': 0,
330
        'visible_for_client': True,
331
        'description': (
332
            'Amount of fake results generated per each host in the target '
333
            + 'list for a dry run scan.'
334
        ),
335
    },
336
}
337
338
VT_BASE_OID = "1.3.6.1.4.1.25623."
339
340
341
def safe_int(value: str) -> Optional[int]:
342
    """Convert a string into an integer and return None in case of errors
343
    during conversion
344
    """
345
    try:
346
        return int(value)
347
    except (ValueError, TypeError):
348
        return None
349
350
351
class OpenVasVtsFilter(VtsFilter):
352
353
    """Methods to overwrite the ones in the original class."""
354
355
    def __init__(self, nvticache: NVTICache) -> None:
356
        super().__init__()
357
358
        self.nvti = nvticache
359
360
    def format_vt_modification_time(self, value: str) -> str:
361
        """Convert the string seconds since epoch into a 19 character
362
        string representing YearMonthDayHourMinuteSecond,
363
        e.g. 20190319122532. This always refers to UTC.
364
        """
365
366
        return datetime.utcfromtimestamp(int(value)).strftime("%Y%m%d%H%M%S")
367
368
    def get_filtered_vts_list(self, vts, vt_filter: str) -> Optional[List[str]]:
369
        """Gets a collection of vulnerability test from the redis cache,
370
        which match the filter.
371
372
        Arguments:
373
            vt_filter: Filter to apply to the vts collection.
374
            vts: The complete vts collection.
375
376
        Returns:
377
            List with filtered vulnerability tests. The list can be empty.
378
            None in case of filter parse failure.
379
        """
380
        filters = self.parse_filters(vt_filter)
381
        if not filters:
382
            return None
383
384
        if not self.nvti:
385
            return None
386
387
        vt_oid_list = [vtlist[1] for vtlist in self.nvti.get_oids()]
388
        vt_oid_list_temp = copy.copy(vt_oid_list)
389
        vthelper = VtHelper(self.nvti)
390
391
        for element, oper, filter_val in filters:
392
            for vt_oid in vt_oid_list_temp:
393
                if vt_oid not in vt_oid_list:
394
                    continue
395
396
                vt = vthelper.get_single_vt(vt_oid)
397
                if vt is None or not vt.get(element):
398
                    vt_oid_list.remove(vt_oid)
399
                    continue
400
401
                elem_val = vt.get(element)
402
                val = self.format_filter_value(element, elem_val)
403
404
                if self.filter_operator[oper](val, filter_val):
405
                    continue
406
                else:
407
                    vt_oid_list.remove(vt_oid)
408
409
        return vt_oid_list
410
411
412
class OSPDopenvas(OSPDaemon):
413
414
    """Class for ospd-openvas daemon."""
415
416
    def __init__(
417
        self, *, niceness=None, lock_file_dir='/var/run/ospd', **kwargs
418
    ):
419
        """Initializes the ospd-openvas daemon's internal data."""
420
        self.main_db = MainDB()
421
        self.nvti = NVTICache(self.main_db)
422
423
        super().__init__(
424
            customvtfilter=OpenVasVtsFilter(self.nvti),
425
            storage=dict,
426
            file_storage_dir=lock_file_dir,
427
            **kwargs,
428
        )
429
430
        self.server_version = __version__
431
432
        self._niceness = str(niceness)
433
434
        self.feed_lock = LockFile(Path(lock_file_dir) / 'feed-update.lock')
435
        self.daemon_info['name'] = 'OSPd OpenVAS'
436
        self.scanner_info['name'] = 'openvas'
437
        self.scanner_info['version'] = ''  # achieved during self.init()
438
        self.scanner_info['description'] = OSPD_DESC
439
440
        for name, param in OSPD_PARAMS.items():
441
            self.set_scanner_param(name, param)
442
443
        self._sudo_available = None
444
        self._is_running_as_root = None
445
446
        self.scan_only_params = dict()
447
448
    def init(self, server: BaseServer) -> None:
449
450
        self.scan_collection.init()
451
452
        server.start(self.handle_client_stream)
453
454
        self.scanner_info['version'] = Openvas.get_version()
455
456
        self.set_params_from_openvas_settings()
457
458
        with self.feed_lock.wait_for_lock():
459
            Openvas.load_vts_into_redis()
460
            current_feed = self.nvti.get_feed_version()
461
            self.set_vts_version(vts_version=current_feed)
462
463
            logger.debug("Calculating vts integrity check hash...")
464
            vthelper = VtHelper(self.nvti)
465
            self.vts.sha256_hash = vthelper.calculate_vts_collection_hash()
466
467
        self.initialized = True
468
469
    def set_params_from_openvas_settings(self):
470
        """Set OSPD_PARAMS with the params taken from the openvas executable."""
471
        param_list = Openvas.get_settings()
472
473
        for elem in param_list:
474
            if elem not in OSPD_PARAMS:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable OSPD_PARAMS does not seem to be defined.
Loading history...
475
                self.scan_only_params[elem] = param_list[elem]
476
            else:
477
                OSPD_PARAMS[elem]['default'] = param_list[elem]
478
479
    def feed_is_outdated(self, current_feed: str) -> Optional[bool]:
480
        """Compare the current feed with the one in the disk.
481
482
        Return:
483
            False if there is no new feed.
484
            True if the feed version in disk is newer than the feed in
485
                redis cache.
486
            None if there is no feed on the disk.
487
        """
488
        plugins_folder = self.scan_only_params.get('plugins_folder')
489
        if not plugins_folder:
490
            raise OspdOpenvasError("Error: Path to plugins folder not found.")
491
492
        feed_info_file = Path(plugins_folder) / 'plugin_feed_info.inc'
493
        if not feed_info_file.exists():
494
            self.set_params_from_openvas_settings()
495
            logger.debug('Plugins feed file %s not found.', feed_info_file)
496
            return None
497
498
        current_feed = safe_int(current_feed)
499
        if current_feed is None:
500
            logger.debug(
501
                "Wrong PLUGIN_SET format in plugins feed file %s. Format has to"
502
                " be yyyymmddhhmm. For example 'PLUGIN_SET = \"201910251033\"'",
503
                feed_info_file,
504
            )
505
506
        feed_date = None
507
        with feed_info_file.open() as fcontent:
508
            for line in fcontent:
509
                if "PLUGIN_SET" in line:
510
                    feed_date = line.split('=', 1)[1]
511
                    feed_date = feed_date.strip()
512
                    feed_date = feed_date.replace(';', '')
513
                    feed_date = feed_date.replace('"', '')
514
                    feed_date = safe_int(feed_date)
515
                    break
516
517
        logger.debug("Current feed version: %s", current_feed)
518
        logger.debug("Plugin feed version: %s", feed_date)
519
520
        return (
521
            (not feed_date) or (not current_feed) or (current_feed < feed_date)
522
        )
523
524
    def check_feed(self):
525
        """Check if there is a feed update.
526
527
        Wait until all the running scans finished. Set a flag to announce there
528
        is a pending feed update, which avoids to start a new scan.
529
        """
530
        if not self.vts.is_cache_available:
531
            return
532
533
        current_feed = self.nvti.get_feed_version()
534
        is_outdated = self.feed_is_outdated(current_feed)
535
536
        # Check if the nvticache in redis is outdated
537
        if not current_feed or is_outdated:
538
            with self.feed_lock as fl:
539
                if fl.has_lock():
540
                    self.initialized = False
541
                    Openvas.load_vts_into_redis()
542
                    current_feed = self.nvti.get_feed_version()
543
                    self.set_vts_version(vts_version=current_feed)
544
545
                    vthelper = VtHelper(self.nvti)
546
                    self.vts.sha256_hash = (
547
                        vthelper.calculate_vts_collection_hash()
548
                    )
549
                    self.initialized = True
550
                else:
551
                    logger.debug(
552
                        "The feed was not upload or it is outdated, "
553
                        "but other process is locking the update. "
554
                        "Trying again later..."
555
                    )
556
                    return
557
558
    def scheduler(self):
559
        """This method is called periodically to run tasks."""
560
        self.check_feed()
561
562
    def get_vt_iterator(
563
        self, vt_selection: List[str] = None, details: bool = True
564
    ) -> Iterator[Tuple[str, Dict]]:
565
        vthelper = VtHelper(self.nvti)
566
        return vthelper.get_vt_iterator(vt_selection, details)
567
568
    @staticmethod
569
    def get_custom_vt_as_xml_str(vt_id: str, custom: Dict) -> str:
570
        """Return an xml element with custom metadata formatted as string.
571
        Arguments:
572
            vt_id: VT OID. Only used for logging in error case.
573
            custom: Dictionary with the custom metadata.
574
        Return:
575
            Xml element as string.
576
        """
577
578
        _custom = Element('custom')
579
        for key, val in custom.items():
580
            xml_key = SubElement(_custom, key)
581
            try:
582
                xml_key.text = val
583
            except ValueError as e:
584
                logger.warning(
585
                    "Not possible to parse custom tag for VT %s: %s", vt_id, e
586
                )
587
        return tostring(_custom).decode('utf-8')
588
589
    @staticmethod
590
    def get_severities_vt_as_xml_str(vt_id: str, severities: Dict) -> str:
591
        """Return an xml element with severities as string.
592
        Arguments:
593
            vt_id: VT OID. Only used for logging in error case.
594
            severities: Dictionary with the severities.
595
        Return:
596
            Xml element as string.
597
        """
598
        _severities = Element('severities')
599
        _severity = SubElement(_severities, 'severity')
600
        if 'severity_base_vector' in severities:
601
            try:
602
                _value = SubElement(_severity, 'value')
603
                _value.text = severities.get('severity_base_vector')
604
            except ValueError as e:
605
                logger.warning(
606
                    "Not possible to parse severity tag for vt %s: %s", vt_id, e
607
                )
608
        if 'severity_origin' in severities:
609
            _origin = SubElement(_severity, 'origin')
610
            _origin.text = severities.get('severity_origin')
611
        if 'severity_date' in severities:
612
            _date = SubElement(_severity, 'date')
613
            _date.text = severities.get('severity_date')
614
        if 'severity_type' in severities:
615
            _severity.set('type', severities.get('severity_type'))
616
617
        return tostring(_severities).decode('utf-8')
618
619
    @staticmethod
620
    def get_params_vt_as_xml_str(vt_id: str, vt_params: Dict) -> str:
621
        """Return an xml element with params formatted as string.
622
        Arguments:
623
            vt_id: VT OID. Only used for logging in error case.
624
            vt_params: Dictionary with the VT parameters.
625
        Return:
626
            Xml element as string.
627
        """
628
        vt_params_xml = Element('params')
629
        for _pref_id, prefs in vt_params.items():
630
            vt_param = Element('param')
631
            vt_param.set('type', prefs['type'])
632
            vt_param.set('id', _pref_id)
633
            xml_name = SubElement(vt_param, 'name')
634
            try:
635
                xml_name.text = prefs['name']
636
            except ValueError as e:
637
                logger.warning(
638
                    "Not possible to parse parameter for VT %s: %s", vt_id, e
639
                )
640
            if prefs['default']:
641
                xml_def = SubElement(vt_param, 'default')
642
                try:
643
                    xml_def.text = prefs['default']
644
                except ValueError as e:
645
                    logger.warning(
646
                        "Not possible to parse default parameter for VT %s: %s",
647
                        vt_id,
648
                        e,
649
                    )
650
            vt_params_xml.append(vt_param)
651
652
        return tostring(vt_params_xml).decode('utf-8')
653
654
    @staticmethod
655
    def get_refs_vt_as_xml_str(vt_id: str, vt_refs: Dict) -> str:
656
        """Return an xml element with references formatted as string.
657
        Arguments:
658
            vt_id: VT OID. Only used for logging in error case.
659
            vt_refs: Dictionary with the VT references.
660
        Return:
661
            Xml element as string.
662
        """
663
        vt_refs_xml = Element('refs')
664
        for ref_type, ref_values in vt_refs.items():
665
            for value in ref_values:
666
                vt_ref = Element('ref')
667
                if ref_type == "xref" and value:
668
                    for xref in value.split(', '):
669
                        try:
670
                            _type, _id = xref.split(':', 1)
671
                        except ValueError as e:
672
                            logger.error(
673
                                'Not possible to parse xref "%s" for VT %s: %s',
674
                                xref,
675
                                vt_id,
676
                                e,
677
                            )
678
                            continue
679
                        vt_ref.set('type', _type.lower())
680
                        vt_ref.set('id', _id)
681
                elif value:
682
                    vt_ref.set('type', ref_type.lower())
683
                    vt_ref.set('id', value)
684
                else:
685
                    continue
686
                vt_refs_xml.append(vt_ref)
687
688
        return tostring(vt_refs_xml).decode('utf-8')
689
690
    @staticmethod
691
    def get_dependencies_vt_as_xml_str(
692
        vt_id: str, vt_dependencies: List
693
    ) -> str:
694
        """Return  an xml element with dependencies as string.
695
        Arguments:
696
            vt_id: VT OID. Only used for logging in error case.
697
            vt_dependencies: List with the VT dependencies.
698
        Return:
699
            Xml element as string.
700
        """
701
        vt_deps_xml = Element('dependencies')
702
        for dep in vt_dependencies:
703
            _vt_dep = Element('dependency')
704
            if VT_BASE_OID in dep:
705
                _vt_dep.set('vt_id', dep)
706
            else:
707
                logger.error(
708
                    'Not possible to add dependency %s for VT %s', dep, vt_id
709
                )
710
                continue
711
            vt_deps_xml.append(_vt_dep)
712
713
        return tostring(vt_deps_xml).decode('utf-8')
714
715
    @staticmethod
716
    def get_creation_time_vt_as_xml_str(
717
        vt_id: str, vt_creation_time: str
718
    ) -> str:
719
        """Return creation time as string.
720
        Arguments:
721
            vt_id: VT OID. Only used for logging in error case.
722
            vt_creation_time: String with the VT creation time.
723
        Return:
724
           Xml element as string.
725
        """
726
        _time = Element('creation_time')
727
        try:
728
            _time.text = vt_creation_time
729
        except ValueError as e:
730
            logger.warning(
731
                "Not possible to parse creation time for VT %s: %s", vt_id, e
732
            )
733
        return tostring(_time).decode('utf-8')
734
735
    @staticmethod
736
    def get_modification_time_vt_as_xml_str(
737
        vt_id: str, vt_modification_time: str
738
    ) -> str:
739
        """Return modification time as string.
740
        Arguments:
741
            vt_id: VT OID. Only used for logging in error case.
742
            vt_modification_time: String with the VT modification time.
743
        Return:
744
            Xml element as string.
745
        """
746
        _time = Element('modification_time')
747
        try:
748
            _time.text = vt_modification_time
749
        except ValueError as e:
750
            logger.warning(
751
                "Not possible to parse modification time for VT %s: %s",
752
                vt_id,
753
                e,
754
            )
755
        return tostring(_time).decode('utf-8')
756
757
    @staticmethod
758
    def get_summary_vt_as_xml_str(vt_id: str, summary: str) -> str:
759
        """Return summary as string.
760
        Arguments:
761
            vt_id: VT OID. Only used for logging in error case.
762
            summary: String with a VT summary.
763
        Return:
764
            Xml element as string.
765
        """
766
        _summary = Element('summary')
767
        try:
768
            _summary.text = summary
769
        except ValueError as e:
770
            logger.warning(
771
                "Not possible to parse summary tag for VT %s: %s", vt_id, e
772
            )
773
        return tostring(_summary).decode('utf-8')
774
775
    @staticmethod
776
    def get_impact_vt_as_xml_str(vt_id: str, impact) -> str:
777
        """Return impact as string.
778
779
        Arguments:
780
            vt_id (str): VT OID. Only used for logging in error case.
781
            impact (str): String which explain the vulneravility impact.
782
        Return:
783
            string: xml element as string.
784
        """
785
        _impact = Element('impact')
786
        try:
787
            _impact.text = impact
788
        except ValueError as e:
789
            logger.warning(
790
                "Not possible to parse impact tag for VT %s: %s", vt_id, e
791
            )
792
        return tostring(_impact).decode('utf-8')
793
794
    @staticmethod
795
    def get_affected_vt_as_xml_str(vt_id: str, affected: str) -> str:
796
        """Return affected as string.
797
        Arguments:
798
            vt_id: VT OID. Only used for logging in error case.
799
            affected: String which explain what is affected.
800
        Return:
801
            Xml element as string.
802
        """
803
        _affected = Element('affected')
804
        try:
805
            _affected.text = affected
806
        except ValueError as e:
807
            logger.warning(
808
                "Not possible to parse affected tag for VT %s: %s", vt_id, e
809
            )
810
        return tostring(_affected).decode('utf-8')
811
812
    @staticmethod
813
    def get_insight_vt_as_xml_str(vt_id: str, insight: str) -> str:
814
        """Return insight as string.
815
        Arguments:
816
            vt_id: VT OID. Only used for logging in error case.
817
            insight: String giving an insight of the vulnerability.
818
        Return:
819
            Xml element as string.
820
        """
821
        _insight = Element('insight')
822
        try:
823
            _insight.text = insight
824
        except ValueError as e:
825
            logger.warning(
826
                "Not possible to parse insight tag for VT %s: %s", vt_id, e
827
            )
828
        return tostring(_insight).decode('utf-8')
829
830
    @staticmethod
831
    def get_solution_vt_as_xml_str(
832
        vt_id: str,
833
        solution: str,
834
        solution_type: Optional[str] = None,
835
        solution_method: Optional[str] = None,
836
    ) -> str:
837
        """Return solution as string.
838
        Arguments:
839
            vt_id: VT OID. Only used for logging in error case.
840
            solution: String giving a possible solution.
841
            solution_type: A solution type
842
            solution_method: A solution method
843
        Return:
844
            Xml element as string.
845
        """
846
        _solution = Element('solution')
847
        try:
848
            _solution.text = solution
849
        except ValueError as e:
850
            logger.warning(
851
                "Not possible to parse solution tag for VT %s: %s", vt_id, e
852
            )
853
        if solution_type:
854
            _solution.set('type', solution_type)
855
        if solution_method:
856
            _solution.set('method', solution_method)
857
        return tostring(_solution).decode('utf-8')
858
859
    @staticmethod
860
    def get_detection_vt_as_xml_str(
861
        vt_id: str,
862
        detection: Optional[str] = None,
863
        qod_type: Optional[str] = None,
864
        qod: Optional[str] = None,
865
    ) -> str:
866
        """Return detection as string.
867
        Arguments:
868
            vt_id: VT OID. Only used for logging in error case.
869
            detection: String which explain how the vulnerability
870
              was detected.
871
            qod_type: qod type.
872
            qod: qod value.
873
        Return:
874
            Xml element as string.
875
        """
876
        _detection = Element('detection')
877
        if detection:
878
            try:
879
                _detection.text = detection
880
            except ValueError as e:
881
                logger.warning(
882
                    "Not possible to parse detection tag for VT %s: %s",
883
                    vt_id,
884
                    e,
885
                )
886
        if qod_type:
887
            _detection.set('qod_type', qod_type)
888
        elif qod:
889
            _detection.set('qod', qod)
890
891
        return tostring(_detection).decode('utf-8')
892
893
    @property
894
    def is_running_as_root(self) -> bool:
895
        """Check if it is running as root user."""
896
        if self._is_running_as_root is not None:
897
            return self._is_running_as_root
898
899
        self._is_running_as_root = False
900
        if geteuid() == 0:
901
            self._is_running_as_root = True
902
903
        return self._is_running_as_root
904
905
    @property
906
    def sudo_available(self) -> bool:
907
        """Checks that sudo is available"""
908
        if self._sudo_available is not None:
909
            return self._sudo_available
910
911
        if self.is_running_as_root:
912
            self._sudo_available = False
913
            return self._sudo_available
914
915
        self._sudo_available = Openvas.check_sudo()
916
917
        return self._sudo_available
918
919
    def check(self) -> bool:
920
        """Checks that openvas command line tool is found and
921
        is executable."""
922
        has_openvas = Openvas.check()
923
        if not has_openvas:
924
            logger.error(
925
                'openvas executable not available. Please install openvas'
926
                ' into your PATH.'
927
            )
928
        return has_openvas
929
930
    def report_openvas_scan_status(self, kbdb: BaseDB, scan_id: str):
931
        """Get all status entries from redis kb.
932
933
        Arguments:
934
            kbdb: KB context where to get the status from.
935
            scan_id: Scan ID to identify the current scan.
936
        """
937
        all_status = kbdb.get_scan_status()
938
        all_hosts = dict()
939
        finished_hosts = list()
940
        for res in all_status:
941
            try:
942
                current_host, launched, total = res.split('/')
943
            except ValueError:
944
                continue
945
946
            try:
947
                if float(total) == 0:
948
                    continue
949
                elif float(total) == ScanProgress.DEAD_HOST:
950
                    host_prog = ScanProgress.DEAD_HOST
951
                else:
952
                    host_prog = int((float(launched) / float(total)) * 100)
953
            except TypeError:
954
                continue
955
956
            all_hosts[current_host] = host_prog
957
958
            if (
959
                host_prog == ScanProgress.DEAD_HOST
960
                or host_prog == ScanProgress.FINISHED
961
            ):
962
                finished_hosts.append(current_host)
963
964
            logger.debug(
965
                '%s: Host %s has progress: %d', scan_id, current_host, host_prog
966
            )
967
968
        self.set_scan_progress_batch(scan_id, host_progress=all_hosts)
969
970
        self.sort_host_finished(scan_id, finished_hosts)
971
972
    def report_openvas_results(self, db: BaseDB, scan_id: str) -> bool:
973
        """Get all result entries from redis kb."""
974
975
        vthelper = VtHelper(self.nvti)
976
977
        # Result messages come in the next form, with optional uri field
978
        # type ||| host ip ||| hostname ||| port ||| OID ||| value [|||uri]
979
        all_results = db.get_result()
980
        res_list = ResultList()
981
        total_dead = 0
982
        for res in all_results:
983
            if not res:
984
                continue
985
986
            msg = res.split('|||')
987
            roid = msg[4].strip()
988
            rqod = ''
989
            rname = ''
990
            current_host = msg[1].strip() if msg[1] else ''
991
            rhostname = msg[2].strip() if msg[2] else ''
992
            host_is_dead = "Host dead" in msg[5] or msg[0] == "DEADHOST"
993
            host_deny = "Host access denied" in msg[5]
994
            start_end_msg = msg[0] == "HOST_START" or msg[0] == "HOST_END"
995
            host_count = msg[0] == "HOSTS_COUNT"
996
            vt_aux = None
997
998
            # URI is optional and msg list length must be checked
999
            ruri = ''
1000
            if len(msg) > 6:
1001
                ruri = msg[6]
1002
1003
            if (
1004
                roid
1005
                and not host_is_dead
1006
                and not host_deny
1007
                and not start_end_msg
1008
                and not host_count
1009
            ):
1010
                vt_aux = vthelper.get_single_vt(roid)
1011
1012
            if (
1013
                not vt_aux
1014
                and not host_is_dead
1015
                and not host_deny
1016
                and not start_end_msg
1017
                and not host_count
1018
            ):
1019
                logger.warning('Invalid VT oid %s for a result', roid)
1020
1021
            if vt_aux:
1022
                if vt_aux.get('qod_type'):
1023
                    qod_t = vt_aux.get('qod_type')
1024
                    rqod = self.nvti.QOD_TYPES[qod_t]
1025
                elif vt_aux.get('qod'):
1026
                    rqod = vt_aux.get('qod')
1027
1028
                rname = vt_aux.get('name')
1029
1030
            if msg[0] == 'ERRMSG':
1031
                res_list.add_scan_error_to_list(
1032
                    host=current_host,
1033
                    hostname=rhostname,
1034
                    name=rname,
1035
                    value=msg[5],
1036
                    port=msg[3],
1037
                    test_id=roid,
1038
                    uri=ruri,
1039
                )
1040
1041
            elif msg[0] == 'HOST_START' or msg[0] == 'HOST_END':
1042
                res_list.add_scan_log_to_list(
1043
                    host=current_host,
1044
                    name=msg[0],
1045
                    value=msg[5],
1046
                )
1047
1048
            elif msg[0] == 'LOG':
1049
                res_list.add_scan_log_to_list(
1050
                    host=current_host,
1051
                    hostname=rhostname,
1052
                    name=rname,
1053
                    value=msg[5],
1054
                    port=msg[3],
1055
                    qod=rqod,
1056
                    test_id=roid,
1057
                    uri=ruri,
1058
                )
1059
1060
            elif msg[0] == 'HOST_DETAIL':
1061
                res_list.add_scan_host_detail_to_list(
1062
                    host=current_host,
1063
                    hostname=rhostname,
1064
                    name=rname,
1065
                    value=msg[5],
1066
                    uri=ruri,
1067
                )
1068
1069
            elif msg[0] == 'ALARM':
1070
                rseverity = vthelper.get_severity_score(vt_aux)
1071
                res_list.add_scan_alarm_to_list(
1072
                    host=current_host,
1073
                    hostname=rhostname,
1074
                    name=rname,
1075
                    value=msg[5],
1076
                    port=msg[3],
1077
                    test_id=roid,
1078
                    severity=rseverity,
1079
                    qod=rqod,
1080
                    uri=ruri,
1081
                )
1082
1083
            # To process non-scanned dead hosts when
1084
            # test_alive_host_only in openvas is enable
1085
            elif msg[0] == 'DEADHOST':
1086
                try:
1087
                    total_dead = int(msg[5])
1088
                except TypeError:
1089
                    logger.debug('Error processing dead host count')
1090
1091
            # To update total host count
1092
            if msg[0] == 'HOSTS_COUNT':
1093
                try:
1094
                    count_total = int(msg[5])
1095
                    logger.debug(
1096
                        '%s: Set total hosts counted by OpenVAS: %d',
1097
                        scan_id,
1098
                        count_total,
1099
                    )
1100
                    self.set_scan_total_hosts(scan_id, count_total)
1101
                except TypeError:
1102
                    logger.debug('Error processing total host count')
1103
1104
        # Insert result batch into the scan collection table.
1105
        if len(res_list):
1106
            self.scan_collection.add_result_list(scan_id, res_list)
1107
            logger.debug(
1108
                '%s: Inserting %d results into scan collection table',
1109
                scan_id,
1110
                len(res_list),
1111
            )
1112
        if total_dead:
1113
            logger.debug(
1114
                '%s: Set dead hosts counted by OpenVAS: %d',
1115
                scan_id,
1116
                total_dead,
1117
            )
1118
            self.scan_collection.set_amount_dead_hosts(
1119
                scan_id, total_dead=total_dead
1120
            )
1121
1122
        return len(res_list) > 0
1123
1124
    def is_openvas_process_alive(
1125
        self, kbdb: BaseDB, ovas_pid: str, scan_id: str
1126
    ) -> bool:
1127
        parent_exists = True
1128
        parent = None
1129
        try:
1130
            parent = psutil.Process(int(ovas_pid))
1131
        except psutil.NoSuchProcess:
1132
            logger.debug('Process with pid %s already stopped', ovas_pid)
1133
            parent_exists = False
1134
        except TypeError:
1135
            logger.debug(
1136
                'Scan with ID %s never started and stopped unexpectedly',
1137
                scan_id,
1138
            )
1139
            parent_exists = False
1140
1141
        is_zombie = False
1142
        if parent and parent.status() == psutil.STATUS_ZOMBIE:
1143
            logger.debug(
1144
                ' %s: OpenVAS process is a zombie process',
1145
                scan_id,
1146
            )
1147
            is_zombie = True
1148
1149
        if (not parent_exists or is_zombie) and kbdb:
1150
            if kbdb and kbdb.scan_is_stopped(scan_id):
1151
                return True
1152
            return False
1153
1154
        return True
1155
1156
    def stop_scan_cleanup(  # pylint: disable=arguments-differ
1157
        self, scan_id: str
1158
    ):
1159
        """Set a key in redis to indicate the wrapper is stopped.
1160
        It is done through redis because it is a new multiprocess
1161
        instance and it is not possible to reach the variables
1162
        of the grandchild process.
1163
        Indirectly sends SIGUSR1 to the running openvas scan process
1164
        via an invocation of openvas with the --scan-stop option to
1165
        stop it."""
1166
1167
        kbdb = self.main_db.find_kb_database_by_scan_id(scan_id)
1168
        if kbdb:
1169
            kbdb.stop_scan(scan_id)
1170
            ovas_pid = kbdb.get_scan_process_id()
1171
1172
            parent = None
1173
            try:
1174
                parent = psutil.Process(int(ovas_pid))
1175
            except psutil.NoSuchProcess:
1176
                logger.debug('Process with pid %s already stopped', ovas_pid)
1177
            except TypeError:
1178
                logger.debug(
1179
                    'Scan with ID %s never started and stopped unexpectedly',
1180
                    scan_id,
1181
                )
1182
1183
            if parent:
1184
                can_stop_scan = Openvas.stop_scan(
1185
                    scan_id,
1186
                    not self.is_running_as_root and self.sudo_available,
1187
                )
1188
                if not can_stop_scan:
1189
                    logger.debug(
1190
                        'Not possible to stop scan process: %s.',
1191
                        parent,
1192
                    )
1193
                    return False
1194
1195
                logger.debug('Stopping process: %s', parent)
1196
1197
                while parent:
1198
                    if parent.is_running():
1199
                        time.sleep(0.1)
1200
                    else:
1201
                        parent = None
1202
1203
            for scan_db in kbdb.get_scan_databases():
1204
                self.main_db.release_database(scan_db)
1205
1206
    def exec_scan(self, scan_id: str):
1207
        """Starts the OpenVAS scanner for scan_id scan."""
1208
        params = self.scan_collection.get_options(scan_id)
1209
        if params.get("dry_run"):
1210
            dryrun = DryRun(self)
1211
            dryrun.exec_dry_run_scan(scan_id, self.nvti, OSPD_PARAMS)
1212
            return
1213
1214
        do_not_launch = False
1215
        kbdb = self.main_db.get_new_kb_database()
1216
        scan_prefs = PreferenceHandler(
1217
            scan_id, kbdb, self.scan_collection, self.nvti
1218
        )
1219
        kbdb.add_scan_id(scan_id)
1220
        scan_prefs.prepare_target_for_openvas()
1221
1222
        if not scan_prefs.prepare_ports_for_openvas():
1223
            self.add_scan_error(
1224
                scan_id, name='', host='', value='Invalid port list.'
1225
            )
1226
            do_not_launch = True
1227
1228
        # Set credentials
1229
        if not scan_prefs.prepare_credentials_for_openvas():
1230
            error = (
1231
                'All authentifications contain errors.'
1232
                + 'Starting unauthenticated scan instead.'
1233
            )
1234
            self.add_scan_error(
1235
                scan_id,
1236
                name='',
1237
                host='',
1238
                value=error,
1239
            )
1240
            logger.error(error)
1241
        errors = scan_prefs.get_error_messages()
1242
        for e in errors:
1243
            error = 'Malformed credential. ' + e
1244
            self.add_scan_error(
1245
                scan_id,
1246
                name='',
1247
                host='',
1248
                value=error,
1249
            )
1250
            logger.error(error)
1251
1252
        if not scan_prefs.prepare_plugins_for_openvas():
1253
            self.add_scan_error(
1254
                scan_id, name='', host='', value='No VTS to run.'
1255
            )
1256
            do_not_launch = True
1257
1258
        scan_prefs.prepare_main_kbindex_for_openvas()
1259
        scan_prefs.prepare_host_options_for_openvas()
1260
        scan_prefs.prepare_scan_params_for_openvas(OSPD_PARAMS)
1261
        scan_prefs.prepare_reverse_lookup_opt_for_openvas()
1262
        scan_prefs.prepare_alive_test_option_for_openvas()
1263
1264
        # VT preferences are stored after all preferences have been processed,
1265
        # since alive tests preferences have to be able to overwrite default
1266
        # preferences of ping_host.nasl for the classic method.
1267
        scan_prefs.prepare_nvt_preferences()
1268
        scan_prefs.prepare_boreas_alive_test()
1269
1270
        # Release memory used for scan preferences.
1271
        del scan_prefs
1272
1273
        if do_not_launch or kbdb.scan_is_stopped(scan_id):
1274
            self.main_db.release_database(kbdb)
1275
            return
1276
1277
        result = Openvas.start_scan(
1278
            scan_id,
1279
            not self.is_running_as_root and self.sudo_available,
1280
            self._niceness,
1281
        )
1282
1283
        if result is None:
1284
            self.main_db.release_database(kbdb)
1285
            return
1286
1287
        ovas_pid = result.pid
1288
        kbdb.add_scan_process_id(ovas_pid)
1289
        logger.debug('pid = %s', ovas_pid)
1290
1291
        # Wait until the scanner starts and loads all the preferences.
1292
        while kbdb.get_status(scan_id) == 'new':
1293
            res = result.poll()
1294
            if res and res < 0:
1295
                self.stop_scan_cleanup(scan_id)
1296
                logger.error(
1297
                    'It was not possible run the task %s, since openvas ended '
1298
                    'unexpectedly with errors during launching.',
1299
                    scan_id,
1300
                )
1301
                return
1302
1303
            time.sleep(1)
1304
1305
        got_results = False
1306
        while True:
1307
            target_is_finished = kbdb.target_is_finished(scan_id)
1308
            openvas_process_is_alive = self.is_openvas_process_alive(
1309
                kbdb, ovas_pid, scan_id
1310
            )
1311
            if not target_is_finished and not openvas_process_is_alive:
1312
                logger.error(
1313
                    'Task %s was unexpectedly stopped or killed.',
1314
                    scan_id,
1315
                )
1316
                self.add_scan_error(
1317
                    scan_id,
1318
                    name='',
1319
                    host='',
1320
                    value='Task was unexpectedly stopped or killed.',
1321
                )
1322
1323
                # check for scanner error messages before leaving.
1324
                self.report_openvas_results(kbdb, scan_id)
1325
1326
                kbdb.stop_scan(scan_id)
1327
1328
                for scan_db in kbdb.get_scan_databases():
1329
                    self.main_db.release_database(scan_db)
1330
                self.main_db.release_database(kbdb)
1331
                return
1332
1333
            # Wait a second before trying to get result from redis if there
1334
            # was no results before.
1335
            # Otherwise, wait 50 msec to give access other process to redis.
1336
            if not got_results:
1337
                time.sleep(1)
1338
            else:
1339
                time.sleep(0.05)
1340
            got_results = False
1341
1342
            # Check if the client stopped the whole scan
1343
            if kbdb.scan_is_stopped(scan_id):
1344
                logger.debug('%s: Scan stopped by the client', scan_id)
1345
1346
                # clean main_db, but wait for scanner to finish.
1347
                while not kbdb.target_is_finished(scan_id):
1348
                    logger.debug('%s: Waiting the scan to finish', scan_id)
1349
                    time.sleep(1)
1350
                self.main_db.release_database(kbdb)
1351
                return
1352
1353
            got_results = self.report_openvas_results(kbdb, scan_id)
1354
            self.report_openvas_scan_status(kbdb, scan_id)
1355
1356
            # Scan end. No kb in use for this scan id
1357
            if kbdb.target_is_finished(scan_id):
1358
                logger.debug('%s: Target is finished', scan_id)
1359
                break
1360
1361
        # Delete keys from KB related to this scan task.
1362
        logger.debug('%s: End Target. Release main database', scan_id)
1363
        self.main_db.release_database(kbdb)
1364
1365
1366
def main():
1367
    """OSP openvas main function."""
1368
    daemon_main('OSPD - openvas', OSPDopenvas)
1369
1370
1371
if __name__ == '__main__':
1372
    main()
1373