Passed
Pull Request — master (#374)
by
unknown
01:23
created

ospd_openvas.daemon.OSPDopenvas.update_progress()   A

Complexity

Conditions 5

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nop 4
dl 0
loc 25
rs 9.1832
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.cvss import CVSS
42
from ospd.vtfilter import VtsFilter
43
from ospd.resultlist import ResultList
44
45
from ospd_openvas import __version__
46
from ospd_openvas.errors import OspdOpenvasError
47
48
from ospd_openvas.nvticache import NVTICache
49
from ospd_openvas.db import MainDB, BaseDB, ScanDB
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
    '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
362
VT_BASE_OID = "1.3.6.1.4.1.25623."
363
364
365
def 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
375
class 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
436
class OSPDopenvas(OSPDaemon):
437
438
    """ Class for ospd-openvas daemon. """
439
440
    def __init__(
441
        self, *, niceness=None, lock_file_dir='/var/run/ospd', **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:
498
            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...
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
                _severity.text = severities.get('severity_base_vector')
627
            except ValueError as e:
628
                logger.warning(
629
                    "Not possible to parse severity tag for vt %s: %s", vt_id, e
630
                )
631
        if 'severity_origin' in severities:
632
            _severity.set('origin', severities.get('severity_origin'))
633
        if 'severity_type' in severities:
634
            _severity.set('type', severities.get('severity_type'))
635
636
        return tostring(_severities).decode('utf-8')
637
638
    @staticmethod
639
    def get_params_vt_as_xml_str(vt_id: str, vt_params: Dict) -> str:
640
        """Return an xml element with params formatted as string.
641
        Arguments:
642
            vt_id: VT OID. Only used for logging in error case.
643
            vt_params: Dictionary with the VT parameters.
644
        Return:
645
            Xml element as string.
646
        """
647
        vt_params_xml = Element('params')
648
        for _pref_id, prefs in vt_params.items():
649
            vt_param = Element('param')
650
            vt_param.set('type', prefs['type'])
651
            vt_param.set('id', _pref_id)
652
            xml_name = SubElement(vt_param, 'name')
653
            try:
654
                xml_name.text = prefs['name']
655
            except ValueError as e:
656
                logger.warning(
657
                    "Not possible to parse parameter for VT %s: %s", vt_id, e
658
                )
659
            if prefs['default']:
660
                xml_def = SubElement(vt_param, 'default')
661
                try:
662
                    xml_def.text = prefs['default']
663
                except ValueError as e:
664
                    logger.warning(
665
                        "Not possible to parse default parameter for VT %s: %s",
666
                        vt_id,
667
                        e,
668
                    )
669
            vt_params_xml.append(vt_param)
670
671
        return tostring(vt_params_xml).decode('utf-8')
672
673
    @staticmethod
674
    def get_refs_vt_as_xml_str(vt_id: str, vt_refs: Dict) -> str:
675
        """Return an xml element with references formatted as string.
676
        Arguments:
677
            vt_id: VT OID. Only used for logging in error case.
678
            vt_refs: Dictionary with the VT references.
679
        Return:
680
            Xml element as string.
681
        """
682
        vt_refs_xml = Element('refs')
683
        for ref_type, ref_values in vt_refs.items():
684
            for value in ref_values:
685
                vt_ref = Element('ref')
686
                if ref_type == "xref" and value:
687
                    for xref in value.split(', '):
688
                        try:
689
                            _type, _id = xref.split(':', 1)
690
                        except ValueError as e:
691
                            logger.error(
692
                                'Not possible to parse xref "%s" for VT %s: %s',
693
                                xref,
694
                                vt_id,
695
                                e,
696
                            )
697
                            continue
698
                        vt_ref.set('type', _type.lower())
699
                        vt_ref.set('id', _id)
700
                elif value:
701
                    vt_ref.set('type', ref_type.lower())
702
                    vt_ref.set('id', value)
703
                else:
704
                    continue
705
                vt_refs_xml.append(vt_ref)
706
707
        return tostring(vt_refs_xml).decode('utf-8')
708
709
    @staticmethod
710
    def get_dependencies_vt_as_xml_str(
711
        vt_id: str, vt_dependencies: List
712
    ) -> str:
713
        """Return  an xml element with dependencies as string.
714
        Arguments:
715
            vt_id: VT OID. Only used for logging in error case.
716
            vt_dependencies: List with the VT dependencies.
717
        Return:
718
            Xml element as string.
719
        """
720
        vt_deps_xml = Element('dependencies')
721
        for dep in vt_dependencies:
722
            _vt_dep = Element('dependency')
723
            if VT_BASE_OID in dep:
724
                _vt_dep.set('vt_id', dep)
725
            else:
726
                logger.error(
727
                    'Not possible to add dependency %s for VT %s', dep, vt_id
728
                )
729
                continue
730
            vt_deps_xml.append(_vt_dep)
731
732
        return tostring(vt_deps_xml).decode('utf-8')
733
734
    @staticmethod
735
    def get_creation_time_vt_as_xml_str(
736
        vt_id: str, vt_creation_time: str
737
    ) -> str:
738
        """Return creation time as string.
739
        Arguments:
740
            vt_id: VT OID. Only used for logging in error case.
741
            vt_creation_time: String with the VT creation time.
742
        Return:
743
           Xml element as string.
744
        """
745
        _time = Element('creation_time')
746
        try:
747
            _time.text = vt_creation_time
748
        except ValueError as e:
749
            logger.warning(
750
                "Not possible to parse creation time for VT %s: %s", vt_id, e
751
            )
752
        return tostring(_time).decode('utf-8')
753
754
    @staticmethod
755
    def get_modification_time_vt_as_xml_str(
756
        vt_id: str, vt_modification_time: str
757
    ) -> str:
758
        """Return modification time as string.
759
        Arguments:
760
            vt_id: VT OID. Only used for logging in error case.
761
            vt_modification_time: String with the VT modification time.
762
        Return:
763
            Xml element as string.
764
        """
765
        _time = Element('modification_time')
766
        try:
767
            _time.text = vt_modification_time
768
        except ValueError as e:
769
            logger.warning(
770
                "Not possible to parse modification time for VT %s: %s",
771
                vt_id,
772
                e,
773
            )
774
        return tostring(_time).decode('utf-8')
775
776
    @staticmethod
777
    def get_summary_vt_as_xml_str(vt_id: str, summary: str) -> str:
778
        """Return summary as string.
779
        Arguments:
780
            vt_id: VT OID. Only used for logging in error case.
781
            summary: String with a VT summary.
782
        Return:
783
            Xml element as string.
784
        """
785
        _summary = Element('summary')
786
        try:
787
            _summary.text = summary
788
        except ValueError as e:
789
            logger.warning(
790
                "Not possible to parse summary tag for VT %s: %s", vt_id, e
791
            )
792
        return tostring(_summary).decode('utf-8')
793
794
    @staticmethod
795
    def get_impact_vt_as_xml_str(vt_id: str, impact) -> str:
796
        """Return impact as string.
797
798
        Arguments:
799
            vt_id (str): VT OID. Only used for logging in error case.
800
            impact (str): String which explain the vulneravility impact.
801
        Return:
802
            string: xml element as string.
803
        """
804
        _impact = Element('impact')
805
        try:
806
            _impact.text = impact
807
        except ValueError as e:
808
            logger.warning(
809
                "Not possible to parse impact tag for VT %s: %s", vt_id, e
810
            )
811
        return tostring(_impact).decode('utf-8')
812
813
    @staticmethod
814
    def get_affected_vt_as_xml_str(vt_id: str, affected: str) -> str:
815
        """Return affected as string.
816
        Arguments:
817
            vt_id: VT OID. Only used for logging in error case.
818
            affected: String which explain what is affected.
819
        Return:
820
            Xml element as string.
821
        """
822
        _affected = Element('affected')
823
        try:
824
            _affected.text = affected
825
        except ValueError as e:
826
            logger.warning(
827
                "Not possible to parse affected tag for VT %s: %s", vt_id, e
828
            )
829
        return tostring(_affected).decode('utf-8')
830
831
    @staticmethod
832
    def get_insight_vt_as_xml_str(vt_id: str, insight: str) -> str:
833
        """Return insight as string.
834
        Arguments:
835
            vt_id: VT OID. Only used for logging in error case.
836
            insight: String giving an insight of the vulnerability.
837
        Return:
838
            Xml element as string.
839
        """
840
        _insight = Element('insight')
841
        try:
842
            _insight.text = insight
843
        except ValueError as e:
844
            logger.warning(
845
                "Not possible to parse insight tag for VT %s: %s", vt_id, e
846
            )
847
        return tostring(_insight).decode('utf-8')
848
849
    @staticmethod
850
    def get_solution_vt_as_xml_str(
851
        vt_id: str,
852
        solution: str,
853
        solution_type: Optional[str] = None,
854
        solution_method: Optional[str] = None,
855
    ) -> str:
856
        """Return solution as string.
857
        Arguments:
858
            vt_id: VT OID. Only used for logging in error case.
859
            solution: String giving a possible solution.
860
            solution_type: A solution type
861
            solution_method: A solution method
862
        Return:
863
            Xml element as string.
864
        """
865
        _solution = Element('solution')
866
        try:
867
            _solution.text = solution
868
        except ValueError as e:
869
            logger.warning(
870
                "Not possible to parse solution tag for VT %s: %s", vt_id, e
871
            )
872
        if solution_type:
873
            _solution.set('type', solution_type)
874
        if solution_method:
875
            _solution.set('method', solution_method)
876
        return tostring(_solution).decode('utf-8')
877
878
    @staticmethod
879
    def get_detection_vt_as_xml_str(
880
        vt_id: str,
881
        detection: Optional[str] = None,
882
        qod_type: Optional[str] = None,
883
        qod: Optional[str] = None,
884
    ) -> str:
885
        """Return detection as string.
886
        Arguments:
887
            vt_id: VT OID. Only used for logging in error case.
888
            detection: String which explain how the vulnerability
889
              was detected.
890
            qod_type: qod type.
891
            qod: qod value.
892
        Return:
893
            Xml element as string.
894
        """
895
        _detection = Element('detection')
896
        if detection:
897
            try:
898
                _detection.text = detection
899
            except ValueError as e:
900
                logger.warning(
901
                    "Not possible to parse detection tag for VT %s: %s",
902
                    vt_id,
903
                    e,
904
                )
905
        if qod_type:
906
            _detection.set('qod_type', qod_type)
907
        elif qod:
908
            _detection.set('qod', qod)
909
910
        return tostring(_detection).decode('utf-8')
911
912
    @property
913
    def is_running_as_root(self) -> bool:
914
        """ Check if it is running as root user."""
915
        if self._is_running_as_root is not None:
916
            return self._is_running_as_root
917
918
        self._is_running_as_root = False
919
        if geteuid() == 0:
920
            self._is_running_as_root = True
921
922
        return self._is_running_as_root
923
924
    @property
925
    def sudo_available(self) -> bool:
926
        """ Checks that sudo is available """
927
        if self._sudo_available is not None:
928
            return self._sudo_available
929
930
        if self.is_running_as_root:
931
            self._sudo_available = False
932
            return self._sudo_available
933
934
        self._sudo_available = Openvas.check_sudo()
935
936
        return self._sudo_available
937
938
    def check(self) -> bool:
939
        """Checks that openvas command line tool is found and
940
        is executable."""
941
        has_openvas = Openvas.check()
942
        if not has_openvas:
943
            logger.error(
944
                'openvas executable not available. Please install openvas'
945
                ' into your PATH.'
946
            )
947
        return has_openvas
948
949
    def update_progress(self, scan_id: str, current_host: str, msg: str):
950
        """Calculate percentage and update the scan status of a host
951
        for the progress bar.
952
        Arguments:
953
            scan_id: Scan ID to identify the current scan process.
954
            current_host: Host in the target to be updated.
955
            msg: String with launched and total plugins.
956
        """
957
        try:
958
            launched, total = msg.split('/')
959
        except ValueError:
960
            return
961
962
        try:
963
            if float(total) == 0:
964
                return
965
            elif float(total) == ScanProgress.DEAD_HOST:
966
                host_prog = ScanProgress.DEAD_HOST
967
            else:
968
                host_prog = int((float(launched) / float(total)) * 100)
969
        except TypeError:
970
            return
971
972
        self.set_scan_host_progress(
973
            scan_id, host=current_host, progress=host_prog
974
        )
975
976
    def report_openvas_scan_status(
977
        self, scan_db: ScanDB, scan_id: str, current_host: str
978
    ):
979
        """Get all status entries from redis kb.
980
981
        Arguments:
982
            scan_id: Scan ID to identify the current scan.
983
            current_host: Host to be updated.
984
        """
985
        res = scan_db.get_scan_status()
986
        while res:
987
            self.update_progress(scan_id, current_host, res)
988
            res = scan_db.get_scan_status()
989
990
    def get_severity_score(self, vt_aux: dict) -> Optional[float]:
991
        """Return the severity score for the given oid.
992
        Arguments:
993
            vt_aux: VT element from which to get the severity vector
994
        Returns:
995
            The calculated cvss base value. None if there is no severity
996
            vector or severity type is not cvss base version 2.
997
        """
998
        if vt_aux:
999
            severity_type = vt_aux['severities'].get('severity_type')
1000
            severity_vector = vt_aux['severities'].get('severity_base_vector')
1001
1002
            if severity_type == "cvss_base_v2" and severity_vector:
1003
                return CVSS.cvss_base_v2_value(severity_vector)
1004
1005
        return None
1006
1007
    def report_openvas_results(
1008
        self, db: BaseDB, scan_id: str, current_host: str
1009
    ) -> int:
1010
        """ Get all result entries from redis kb. """
1011
1012
        vthelper = VtHelper(self.nvti)
1013
1014
        # Result messages come in the next form, with optional uri field
1015
        # type ||| hostname ||| port ||| OID ||| value [|||uri]
1016
        all_results = db.get_result()
1017
        res_list = ResultList()
1018
        total_dead = 0
1019
        total_results = len(all_results)
1020
1021
        for res in all_results:
1022
            if not res:
1023
                continue
1024
1025
            msg = res.split('|||')
1026
            roid = msg[3].strip()
1027
            rqod = ''
1028
            rname = ''
1029
            rhostname = msg[1].strip() if msg[1] else ''
1030
            host_is_dead = "Host dead" in msg[4] or msg[0] == "DEADHOST"
1031
            host_count = msg[0] == "HOSTS_COUNT"
1032
            host_deny = "Host access denied" in msg[4]
1033
            vt_aux = None
1034
1035
            # URI is optional and msg list length must be checked
1036
            ruri = ''
1037
            if len(msg) > 5:
1038
                ruri = msg[5]
1039
1040
            if roid and not host_is_dead and not host_deny and not host_count:
1041
                vt_aux = vthelper.get_single_vt(roid)
1042
1043
            if (
1044
                not vt_aux
1045
                and not host_is_dead
1046
                and not host_deny
1047
                and not host_count
1048
            ):
1049
                logger.warning('Invalid VT oid %s for a result', roid)
1050
1051
            if vt_aux:
1052
                if vt_aux.get('qod_type'):
1053
                    qod_t = vt_aux.get('qod_type')
1054
                    rqod = self.nvti.QOD_TYPES[qod_t]
1055
                elif vt_aux.get('qod'):
1056
                    rqod = vt_aux.get('qod')
1057
1058
                rname = vt_aux.get('name')
1059
1060
            if msg[0] == 'ERRMSG':
1061
                # Some errors are generated before a host is scanned
1062
                # use the hostname passed in the message if
1063
                # no current host is available.
1064
                if not current_host and rhostname:
1065
                    current_host = rhostname
1066
1067
                res_list.add_scan_error_to_list(
1068
                    host=current_host,
1069
                    hostname=rhostname,
1070
                    name=rname,
1071
                    value=msg[4],
1072
                    port=msg[2],
1073
                    test_id=roid,
1074
                    uri=ruri,
1075
                )
1076
1077
            if msg[0] == 'LOG':
1078
                res_list.add_scan_log_to_list(
1079
                    host=current_host,
1080
                    hostname=rhostname,
1081
                    name=rname,
1082
                    value=msg[4],
1083
                    port=msg[2],
1084
                    qod=rqod,
1085
                    test_id=roid,
1086
                    uri=ruri,
1087
                )
1088
1089
            if msg[0] == 'HOST_DETAIL':
1090
                res_list.add_scan_host_detail_to_list(
1091
                    host=current_host,
1092
                    hostname=rhostname,
1093
                    name=rname,
1094
                    value=msg[4],
1095
                    uri=ruri,
1096
                )
1097
1098
            if msg[0] == 'ALARM':
1099
                rseverity = self.get_severity_score(vt_aux)
1100
                res_list.add_scan_alarm_to_list(
1101
                    host=current_host,
1102
                    hostname=rhostname,
1103
                    name=rname,
1104
                    value=msg[4],
1105
                    port=msg[2],
1106
                    test_id=roid,
1107
                    severity=rseverity,
1108
                    qod=rqod,
1109
                    uri=ruri,
1110
                )
1111
1112
            # To process non-scanned dead hosts when
1113
            # test_alive_host_only in openvas is enable
1114
            if msg[0] == 'DEADHOST':
1115
                try:
1116
                    total_dead = int(msg[4])
1117
                except TypeError:
1118
                    logger.debug('Error processing dead host count')
1119
1120
            # To update total host count
1121
            if msg[0] == 'HOSTS_COUNT':
1122
                try:
1123
                    count_total = int(msg[4])
1124
                    self.set_scan_total_hosts(scan_id, count_total)
1125
                except TypeError:
1126
                    logger.debug('Error processing total host count')
1127
1128
        # Insert result batch into the scan collection table.
1129
        if len(res_list):
1130
            self.scan_collection.add_result_list(scan_id, res_list)
1131
1132
        if total_dead:
1133
            self.scan_collection.set_amount_dead_hosts(
1134
                scan_id, total_dead=total_dead
1135
            )
1136
1137
        return total_results
1138
1139
    def report_openvas_timestamp_scan_host(
1140
        self, scan_db: ScanDB, scan_id: str, host: str
1141
    ):
1142
        """ Get start and end timestamp of a host scan from redis kb. """
1143
        timestamp = scan_db.get_host_scan_end_time()
1144
        if timestamp:
1145
            self.add_scan_log(
1146
                scan_id, host=host, name='HOST_END', value=timestamp
1147
            )
1148
            return
1149
1150
        timestamp = scan_db.get_host_scan_start_time()
1151
        if timestamp:
1152
            self.add_scan_log(
1153
                scan_id, host=host, name='HOST_START', value=timestamp
1154
            )
1155
            return
1156
1157
    def is_openvas_process_alive(
1158
        self, kbdb: BaseDB, ovas_pid: str, openvas_scan_id: str
1159
    ) -> bool:
1160
        parent_exists = True
1161
        parent = None
1162
        try:
1163
            parent = psutil.Process(int(ovas_pid))
1164
        except psutil.NoSuchProcess:
1165
            logger.debug('Process with pid %s already stopped', ovas_pid)
1166
            parent_exists = False
1167
        except TypeError:
1168
            logger.debug(
1169
                'Scan with ID %s never started and stopped unexpectedly',
1170
                openvas_scan_id,
1171
            )
1172
            parent_exists = False
1173
1174
        is_zombie = False
1175
        if parent and parent.status() == psutil.STATUS_ZOMBIE:
1176
            is_zombie = True
1177
1178
        if (not parent_exists or is_zombie) and kbdb:
1179
            if kbdb and kbdb.scan_is_stopped(openvas_scan_id):
1180
                return True
1181
            return False
1182
1183
        return True
1184
1185
    def stop_scan_cleanup(  # pylint: disable=arguments-differ
1186
        self, global_scan_id: str
1187
    ):
1188
        """Set a key in redis to indicate the wrapper is stopped.
1189
        It is done through redis because it is a new multiprocess
1190
        instance and it is not possible to reach the variables
1191
        of the grandchild process.
1192
        Indirectly sends SIGUSR1 to the running openvas scan process
1193
        via an invocation of openvas with the --scan-stop option to
1194
        stop it."""
1195
1196
        openvas_scan_id, kbdb = self.main_db.find_kb_database_by_scan_id(
1197
            global_scan_id
1198
        )
1199
        if kbdb:
1200
            kbdb.stop_scan(openvas_scan_id)
1201
            ovas_pid = kbdb.get_scan_process_id()
1202
1203
            parent = None
1204
            try:
1205
                parent = psutil.Process(int(ovas_pid))
1206
            except psutil.NoSuchProcess:
1207
                logger.debug('Process with pid %s already stopped', ovas_pid)
1208
            except TypeError:
1209
                logger.debug(
1210
                    'Scan with ID %s never started and stopped unexpectedly',
1211
                    openvas_scan_id,
1212
                )
1213
1214
            if parent:
1215
                can_stop_scan = Openvas.stop_scan(
1216
                    openvas_scan_id,
1217
                    not self.is_running_as_root and self.sudo_available,
1218
                )
1219
                if not can_stop_scan:
1220
                    logger.debug(
1221
                        'Not possible to stop scan process: %s.',
1222
                        parent,
1223
                    )
1224
                    return False
1225
1226
                logger.debug('Stopping process: %s', parent)
1227
1228
                while parent:
1229
                    if parent.is_running():
1230
                        time.sleep(0.1)
1231
                    else:
1232
                        parent = None
1233
1234
            for scan_db in kbdb.get_scan_databases():
1235
                self.main_db.release_database(scan_db)
1236
1237
    def exec_scan(self, scan_id: str):
1238
        """ Starts the OpenVAS scanner for scan_id scan. """
1239
        do_not_launch = False
1240
        kbdb = self.main_db.get_new_kb_database()
1241
        scan_prefs = PreferenceHandler(
1242
            scan_id, kbdb, self.scan_collection, self.nvti
1243
        )
1244
        openvas_scan_id = scan_prefs.prepare_openvas_scan_id_for_openvas()
1245
        scan_prefs.prepare_target_for_openvas()
1246
1247
        if not scan_prefs.prepare_ports_for_openvas():
1248
            self.add_scan_error(
1249
                scan_id, name='', host='', value='No port list defined.'
1250
            )
1251
            do_not_launch = True
1252
1253
        # Set credentials
1254
        if not scan_prefs.prepare_credentials_for_openvas():
1255
            self.add_scan_error(
1256
                scan_id, name='', host='', value='Malformed credential.'
1257
            )
1258
            do_not_launch = True
1259
1260
        if not scan_prefs.prepare_plugins_for_openvas():
1261
            self.add_scan_error(
1262
                scan_id, name='', host='', value='No VTS to run.'
1263
            )
1264
            do_not_launch = True
1265
1266
        scan_prefs.prepare_main_kbindex_for_openvas()
1267
        scan_prefs.prepare_host_options_for_openvas()
1268
        scan_prefs.prepare_scan_params_for_openvas(OSPD_PARAMS)
1269
        scan_prefs.prepare_reverse_lookup_opt_for_openvas()
1270
        scan_prefs.prepare_alive_test_option_for_openvas()
1271
1272
        # VT preferences are stored after all preferences have been processed,
1273
        # since alive tests preferences have to be able to overwrite default
1274
        # preferences of ping_host.nasl for the classic method.
1275
        scan_prefs.prepare_nvt_preferences()
1276
        scan_prefs.prepare_boreas_alive_test()
1277
1278
        # Release memory used for scan preferences.
1279
        del scan_prefs
1280
1281
        if do_not_launch or kbdb.scan_is_stopped(openvas_scan_id):
1282
            self.main_db.release_database(kbdb)
1283
            return
1284
1285
        result = Openvas.start_scan(
1286
            openvas_scan_id,
1287
            not self.is_running_as_root and self.sudo_available,
1288
            self._niceness,
1289
        )
1290
1291
        if result is None:
1292
            self.main_db.release_database(kbdb)
1293
            return
1294
1295
        ovas_pid = result.pid
1296
        kbdb.add_scan_process_id(ovas_pid)
1297
        logger.debug('pid = %s', ovas_pid)
1298
1299
        # Wait until the scanner starts and loads all the preferences.
1300
        while kbdb.get_status(openvas_scan_id) == 'new':
1301
            res = result.poll()
1302
            if res and res < 0:
1303
                self.stop_scan_cleanup(scan_id)
1304
                logger.error(
1305
                    'It was not possible run the task %s, since openvas ended '
1306
                    'unexpectedly with errors during launching.',
1307
                    scan_id,
1308
                )
1309
                return
1310
1311
            time.sleep(1)
1312
1313
        no_id_found = False
1314
        got_results = False
1315
        while True:
1316
            if not kbdb.target_is_finished(
1317
                scan_id
1318
            ) and not self.is_openvas_process_alive(
1319
                kbdb, ovas_pid, openvas_scan_id
1320
            ):
1321
                logger.error(
1322
                    'Task %s was unexpectedly stopped or killed.',
1323
                    scan_id,
1324
                )
1325
                self.add_scan_error(
1326
                    scan_id,
1327
                    name='',
1328
                    host='',
1329
                    value='Task was unexpectedly stopped or killed.',
1330
                )
1331
                kbdb.stop_scan(openvas_scan_id)
1332
                for scan_db in kbdb.get_scan_databases():
1333
                    self.main_db.release_database(scan_db)
1334
                self.main_db.release_database(kbdb)
1335
                return
1336
1337
            # Wait a second before trying to get result from redis if there
1338
            # was no results before.
1339
            # Otherwise, wait 50 msec to give access other process to redis.
1340
            if not got_results:
1341
                time.sleep(1)
1342
            else:
1343
                time.sleep(0.05)
1344
            got_results = False
1345
1346
            # Check if the client stopped the whole scan
1347
            if kbdb.scan_is_stopped(openvas_scan_id):
1348
                # clean main_db, but wait for scanner to finish.
1349
                while not kbdb.target_is_finished(scan_id):
1350
                    time.sleep(1)
1351
                self.main_db.release_database(kbdb)
1352
                return
1353
1354
            self.report_openvas_results(kbdb, scan_id, "")
1355
1356
            res_count = 0
1357
            for scan_db in kbdb.get_scan_databases():
1358
                id_aux = scan_db.get_scan_id()
1359
                if not id_aux:
1360
                    continue
1361
1362
                if id_aux == openvas_scan_id:
1363
                    no_id_found = False
1364
                    current_host = scan_db.get_host_ip()
1365
1366
                    res_count += self.report_openvas_results(
1367
                        scan_db, scan_id, current_host
1368
                    )
1369
                    if res_count > 0:
1370
                        got_results = True
1371
1372
                    self.report_openvas_scan_status(
1373
                        scan_db, scan_id, current_host
1374
                    )
1375
                    self.report_openvas_timestamp_scan_host(
1376
                        scan_db, scan_id, current_host
1377
                    )
1378
1379
                    if scan_db.host_is_finished(openvas_scan_id):
1380
                        self.report_openvas_scan_status(
1381
                            scan_db, scan_id, current_host
1382
                        )
1383
1384
                        self.report_openvas_timestamp_scan_host(
1385
                            scan_db, scan_id, current_host
1386
                        )
1387
                        if current_host:
1388
                            self.sort_host_finished(
1389
                                scan_id, finished_hosts=current_host
1390
                            )
1391
1392
                        kbdb.remove_scan_database(scan_db)
1393
                        self.main_db.release_database(scan_db)
1394
1395
            # Scan end. No kb in use for this scan id
1396
            if no_id_found and kbdb.target_is_finished(scan_id):
1397
                break
1398
1399
            no_id_found = True
1400
1401
        # Delete keys from KB related to this scan task.
1402
        self.main_db.release_database(kbdb)
1403
1404
1405
def main():
1406
    """ OSP openvas main function. """
1407
    daemon_main('OSPD - openvas', OSPDopenvas)
1408
1409
1410
if __name__ == '__main__':
1411
    main()
1412