ospd_openvas.daemon.OSPDopenvas.exec_scan()   F
last analyzed

Complexity

Conditions 19

Size

Total Lines 163
Code Lines 108

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 108
nop 2
dl 0
loc 163
rs 0.4199
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.exec_scan() 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, ScanStatus
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/lib/openvas', **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:  # pylint: disable=consider-using-dict-items
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
    @staticmethod
1136
    def is_openvas_process_alive(openvas_process: psutil.Popen) -> bool:
1137
1138
        if openvas_process.status() == psutil.STATUS_ZOMBIE:
1139
            logger.debug("Process is a Zombie, waiting for it to clean up")
1140
            openvas_process.wait()
1141
        return openvas_process.is_running()
1142
1143
    def stop_scan_cleanup(
1144
        self,
1145
        kbdb: BaseDB,
1146
        scan_id: str,
1147
        ovas_process: psutil.Popen,  # pylint: disable=arguments-differ
1148
    ):
1149
        """Set a key in redis to indicate the wrapper is stopped.
1150
        It is done through redis because it is a new multiprocess
1151
        instance and it is not possible to reach the variables
1152
        of the grandchild process.
1153
        Indirectly sends SIGUSR1 to the running openvas scan process
1154
        via an invocation of openvas with the --scan-stop option to
1155
        stop it."""
1156
1157
        if kbdb:
1158
            # Set stop flag in redis
1159
            kbdb.stop_scan(scan_id)
1160
1161
            # Check if openvas is running
1162
            if ovas_process.is_running():
1163
                # Cleaning in case of Zombie Process
1164
                if ovas_process.status() == psutil.STATUS_ZOMBIE:
1165
                    logger.debug(
1166
                        '%s: Process with PID %s is a Zombie process.'
1167
                        ' Cleaning up...',
1168
                        scan_id,
1169
                        ovas_process.pid,
1170
                    )
1171
                    ovas_process.wait()
1172
                # Stop openvas process and wait until it stopped
1173
                else:
1174
                    can_stop_scan = Openvas.stop_scan(
1175
                        scan_id,
1176
                        not self.is_running_as_root and self.sudo_available,
1177
                    )
1178
                    if not can_stop_scan:
1179
                        logger.debug(
1180
                            'Not possible to stop scan process: %s.',
1181
                            ovas_process,
1182
                        )
1183
                        return
1184
1185
                    logger.debug('Stopping process: %s', ovas_process)
1186
1187
                    while ovas_process.is_running():
1188
                        if ovas_process.status() == psutil.STATUS_ZOMBIE:
1189
                            ovas_process.wait()
1190
                        else:
1191
                            time.sleep(0.1)
1192
            else:
1193
                logger.debug(
1194
                    "%s: Process with PID %s already stopped",
1195
                    scan_id,
1196
                    ovas_process.pid,
1197
                )
1198
1199
            # Clean redis db
1200
            for scan_db in kbdb.get_scan_databases():
1201
                self.main_db.release_database(scan_db)
1202
1203
    def exec_scan(self, scan_id: str):
1204
        """Starts the OpenVAS scanner for scan_id scan."""
1205
        params = self.scan_collection.get_options(scan_id)
1206
        if params.get("dry_run"):
1207
            dryrun = DryRun(self)
1208
            dryrun.exec_dry_run_scan(scan_id, self.nvti, OSPD_PARAMS)
1209
            return
1210
1211
        do_not_launch = False
1212
        kbdb = self.main_db.get_new_kb_database()
1213
        scan_prefs = PreferenceHandler(
1214
            scan_id, kbdb, self.scan_collection, self.nvti
1215
        )
1216
        kbdb.add_scan_id(scan_id)
1217
        scan_prefs.prepare_target_for_openvas()
1218
1219
        if not scan_prefs.prepare_ports_for_openvas():
1220
            self.add_scan_error(
1221
                scan_id, name='', host='', value='Invalid port list.'
1222
            )
1223
            do_not_launch = True
1224
1225
        # Set credentials
1226
        if not scan_prefs.prepare_credentials_for_openvas():
1227
            error = (
1228
                'All authentifications contain errors.'
1229
                + 'Starting unauthenticated scan instead.'
1230
            )
1231
            self.add_scan_error(
1232
                scan_id,
1233
                name='',
1234
                host='',
1235
                value=error,
1236
            )
1237
            logger.error(error)
1238
        errors = scan_prefs.get_error_messages()
1239
        for e in errors:
1240
            error = 'Malformed credential. ' + e
1241
            self.add_scan_error(
1242
                scan_id,
1243
                name='',
1244
                host='',
1245
                value=error,
1246
            )
1247
            logger.error(error)
1248
1249
        if not scan_prefs.prepare_plugins_for_openvas():
1250
            self.add_scan_error(
1251
                scan_id, name='', host='', value='No VTS to run.'
1252
            )
1253
            do_not_launch = True
1254
1255
        scan_prefs.prepare_main_kbindex_for_openvas()
1256
        scan_prefs.prepare_host_options_for_openvas()
1257
        scan_prefs.prepare_scan_params_for_openvas(OSPD_PARAMS)
1258
        scan_prefs.prepare_reverse_lookup_opt_for_openvas()
1259
        scan_prefs.prepare_alive_test_option_for_openvas()
1260
1261
        # VT preferences are stored after all preferences have been processed,
1262
        # since alive tests preferences have to be able to overwrite default
1263
        # preferences of ping_host.nasl for the classic method.
1264
        scan_prefs.prepare_nvt_preferences()
1265
        scan_prefs.prepare_boreas_alive_test()
1266
1267
        # Release memory used for scan preferences.
1268
        del scan_prefs
1269
1270
        if do_not_launch or kbdb.scan_is_stopped(scan_id):
1271
            self.main_db.release_database(kbdb)
1272
            return
1273
1274
        openvas_process = Openvas.start_scan(
1275
            scan_id,
1276
            not self.is_running_as_root and self.sudo_available,
1277
            self._niceness,
1278
        )
1279
1280
        if openvas_process is None:
1281
            self.main_db.release_database(kbdb)
1282
            return
1283
1284
        kbdb.add_scan_process_id(openvas_process.pid)
1285
        logger.debug('pid = %s', openvas_process.pid)
1286
1287
        # Wait until the scanner starts and loads all the preferences.
1288
        while kbdb.get_status(scan_id) == 'new':
1289
            res = openvas_process.poll()
1290
            if res and res < 0:
1291
                self.stop_scan_cleanup(kbdb, scan_id, openvas_process)
1292
                logger.error(
1293
                    'It was not possible run the task %s, since openvas ended '
1294
                    'unexpectedly with errors during launching.',
1295
                    scan_id,
1296
                )
1297
                return
1298
1299
            time.sleep(1)
1300
1301
        got_results = False
1302
        while True:
1303
1304
            openvas_process_is_alive = self.is_openvas_process_alive(
1305
                openvas_process
1306
            )
1307
            target_is_finished = kbdb.target_is_finished(scan_id)
1308
            scan_stopped = self.get_scan_status(scan_id) == ScanStatus.STOPPED
1309
1310
            # Report new Results and update status
1311
            got_results = self.report_openvas_results(kbdb, scan_id)
1312
            self.report_openvas_scan_status(kbdb, scan_id)
1313
1314
            # Check if the client stopped the whole scan
1315
            if scan_stopped:
1316
                logger.debug('%s: Scan stopped by the client', scan_id)
1317
1318
                self.stop_scan_cleanup(kbdb, scan_id, openvas_process)
1319
1320
                # clean main_db, but wait for scanner to finish.
1321
                while not kbdb.target_is_finished(scan_id):
1322
                    logger.debug('%s: Waiting for openvas to finish', scan_id)
1323
                    time.sleep(1)
1324
                self.main_db.release_database(kbdb)
1325
                return
1326
1327
            # Scan end. No kb in use for this scan id
1328
            if target_is_finished:
1329
                logger.debug('%s: Target is finished', scan_id)
1330
                break
1331
1332
            if not openvas_process_is_alive:
1333
                logger.error(
1334
                    'Task %s was unexpectedly stopped or killed.',
1335
                    scan_id,
1336
                )
1337
                self.add_scan_error(
1338
                    scan_id,
1339
                    name='',
1340
                    host='',
1341
                    value='Task was unexpectedly stopped or killed.',
1342
                )
1343
1344
                # check for scanner error messages before leaving.
1345
                self.report_openvas_results(kbdb, scan_id)
1346
1347
                kbdb.stop_scan(scan_id)
1348
1349
                for scan_db in kbdb.get_scan_databases():
1350
                    self.main_db.release_database(scan_db)
1351
                self.main_db.release_database(kbdb)
1352
                return
1353
1354
            # Wait a second before trying to get result from redis if there
1355
            # was no results before.
1356
            # Otherwise, wait 50 msec to give access other process to redis.
1357
            if not got_results:
1358
                time.sleep(1)
1359
            else:
1360
                time.sleep(0.05)
1361
            got_results = False
1362
1363
        # Delete keys from KB related to this scan task.
1364
        logger.debug('%s: End Target. Release main database', scan_id)
1365
        self.main_db.release_database(kbdb)
1366
1367
1368
def main():
1369
    """OSP openvas main function."""
1370
    daemon_main('OSPD - openvas', OSPDopenvas)
1371
1372
1373
if __name__ == '__main__':
1374
    main()
1375