Passed
Push — master ( f7baf4...fd921d )
by
unknown
02:09 queued 51s
created

OSPDopenvas.report_openvas_results()   F

Complexity

Conditions 31

Size

Total Lines 151
Code Lines 115

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 31
eloc 115
nop 3
dl 0
loc 151
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ospd_openvas.daemon.OSPDopenvas.report_openvas_results() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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