Completed
Push — master ( 448924...170f94 )
by
unknown
16s queued 12s
created

OSPDopenvas.stop_scan_cleanup()   B

Complexity

Conditions 8

Size

Total Lines 59
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 33
nop 4
dl 0
loc 59
rs 7.2213
c 0
b 0
f 0

How to fix   Long Method   

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:

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
        self.openvas_processes = {}
460
461
    def init(self, server: BaseServer) -> None:
462
463
        self.scan_collection.init()
464
465
        server.start(self.handle_client_stream)
466
467
        self.scanner_info['version'] = Openvas.get_version()
468
469
        self.set_params_from_openvas_settings()
470
471
        with self.feed_lock.wait_for_lock():
472
            Openvas.load_vts_into_redis()
473
            current_feed = self.nvti.get_feed_version()
474
            self.set_vts_version(vts_version=current_feed)
475
476
            logger.debug("Calculating vts integrity check hash...")
477
            vthelper = VtHelper(self.nvti)
478
            self.vts.sha256_hash = vthelper.calculate_vts_collection_hash()
479
480
        self.initialized = True
481
482
    def set_params_from_openvas_settings(self):
483
        """Set OSPD_PARAMS with the params taken from the openvas executable."""
484
        param_list = Openvas.get_settings()
485
486
        for elem in param_list:  # pylint: disable=consider-using-dict-items
487
            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...
488
                self.scan_only_params[elem] = param_list[elem]
489
            else:
490
                OSPD_PARAMS[elem]['default'] = param_list[elem]
491
492
    def feed_is_outdated(self, current_feed: str) -> Optional[bool]:
493
        """Compare the current feed with the one in the disk.
494
495
        Return:
496
            False if there is no new feed.
497
            True if the feed version in disk is newer than the feed in
498
                redis cache.
499
            None if there is no feed on the disk.
500
        """
501
        plugins_folder = self.scan_only_params.get('plugins_folder')
502
        if not plugins_folder:
503
            raise OspdOpenvasError("Error: Path to plugins folder not found.")
504
505
        feed_info_file = Path(plugins_folder) / 'plugin_feed_info.inc'
506
        if not feed_info_file.exists():
507
            self.set_params_from_openvas_settings()
508
            logger.debug('Plugins feed file %s not found.', feed_info_file)
509
            return None
510
511
        current_feed = safe_int(current_feed)
512
        if current_feed is None:
513
            logger.debug(
514
                "Wrong PLUGIN_SET format in plugins feed file %s. Format has to"
515
                " be yyyymmddhhmm. For example 'PLUGIN_SET = \"201910251033\"'",
516
                feed_info_file,
517
            )
518
519
        feed_date = None
520
        with feed_info_file.open() as fcontent:
521
            for line in fcontent:
522
                if "PLUGIN_SET" in line:
523
                    feed_date = line.split('=', 1)[1]
524
                    feed_date = feed_date.strip()
525
                    feed_date = feed_date.replace(';', '')
526
                    feed_date = feed_date.replace('"', '')
527
                    feed_date = safe_int(feed_date)
528
                    break
529
530
        logger.debug("Current feed version: %s", current_feed)
531
        logger.debug("Plugin feed version: %s", feed_date)
532
533
        return (
534
            (not feed_date) or (not current_feed) or (current_feed < feed_date)
535
        )
536
537
    def check_feed(self):
538
        """Check if there is a feed update.
539
540
        Wait until all the running scans finished. Set a flag to announce there
541
        is a pending feed update, which avoids to start a new scan.
542
        """
543
        if not self.vts.is_cache_available:
544
            return
545
546
        current_feed = self.nvti.get_feed_version()
547
        is_outdated = self.feed_is_outdated(current_feed)
548
549
        # Check if the nvticache in redis is outdated
550
        if not current_feed or is_outdated:
551
            with self.feed_lock as fl:
552
                if fl.has_lock():
553
                    self.initialized = False
554
                    Openvas.load_vts_into_redis()
555
                    current_feed = self.nvti.get_feed_version()
556
                    self.set_vts_version(vts_version=current_feed)
557
558
                    vthelper = VtHelper(self.nvti)
559
                    self.vts.sha256_hash = (
560
                        vthelper.calculate_vts_collection_hash()
561
                    )
562
                    self.initialized = True
563
                else:
564
                    logger.debug(
565
                        "The feed was not upload or it is outdated, "
566
                        "but other process is locking the update. "
567
                        "Trying again later..."
568
                    )
569
                    return
570
571
    def scheduler(self):
572
        """This method is called periodically to run tasks."""
573
        self.check_feed()
574
575
    def get_vt_iterator(
576
        self, vt_selection: List[str] = None, details: bool = True
577
    ) -> Iterator[Tuple[str, Dict]]:
578
        vthelper = VtHelper(self.nvti)
579
        return vthelper.get_vt_iterator(vt_selection, details)
580
581
    @staticmethod
582
    def get_custom_vt_as_xml_str(vt_id: str, custom: Dict) -> str:
583
        """Return an xml element with custom metadata formatted as string.
584
        Arguments:
585
            vt_id: VT OID. Only used for logging in error case.
586
            custom: Dictionary with the custom metadata.
587
        Return:
588
            Xml element as string.
589
        """
590
591
        _custom = Element('custom')
592
        for key, val in custom.items():
593
            xml_key = SubElement(_custom, key)
594
            try:
595
                xml_key.text = val
596
            except ValueError as e:
597
                logger.warning(
598
                    "Not possible to parse custom tag for VT %s: %s", vt_id, e
599
                )
600
        return tostring(_custom).decode('utf-8')
601
602
    @staticmethod
603
    def get_severities_vt_as_xml_str(vt_id: str, severities: Dict) -> str:
604
        """Return an xml element with severities as string.
605
        Arguments:
606
            vt_id: VT OID. Only used for logging in error case.
607
            severities: Dictionary with the severities.
608
        Return:
609
            Xml element as string.
610
        """
611
        _severities = Element('severities')
612
        _severity = SubElement(_severities, 'severity')
613
        if 'severity_base_vector' in severities:
614
            try:
615
                _value = SubElement(_severity, 'value')
616
                _value.text = severities.get('severity_base_vector')
617
            except ValueError as e:
618
                logger.warning(
619
                    "Not possible to parse severity tag for vt %s: %s", vt_id, e
620
                )
621
        if 'severity_origin' in severities:
622
            _origin = SubElement(_severity, 'origin')
623
            _origin.text = severities.get('severity_origin')
624
        if 'severity_date' in severities:
625
            _date = SubElement(_severity, 'date')
626
            _date.text = severities.get('severity_date')
627
        if 'severity_type' in severities:
628
            _severity.set('type', severities.get('severity_type'))
629
630
        return tostring(_severities).decode('utf-8')
631
632
    @staticmethod
633
    def get_params_vt_as_xml_str(vt_id: str, vt_params: Dict) -> str:
634
        """Return an xml element with params formatted as string.
635
        Arguments:
636
            vt_id: VT OID. Only used for logging in error case.
637
            vt_params: Dictionary with the VT parameters.
638
        Return:
639
            Xml element as string.
640
        """
641
        vt_params_xml = Element('params')
642
        for _pref_id, prefs in vt_params.items():
643
            vt_param = Element('param')
644
            vt_param.set('type', prefs['type'])
645
            vt_param.set('id', _pref_id)
646
            xml_name = SubElement(vt_param, 'name')
647
            try:
648
                xml_name.text = prefs['name']
649
            except ValueError as e:
650
                logger.warning(
651
                    "Not possible to parse parameter for VT %s: %s", vt_id, e
652
                )
653
            if prefs['default']:
654
                xml_def = SubElement(vt_param, 'default')
655
                try:
656
                    xml_def.text = prefs['default']
657
                except ValueError as e:
658
                    logger.warning(
659
                        "Not possible to parse default parameter for VT %s: %s",
660
                        vt_id,
661
                        e,
662
                    )
663
            vt_params_xml.append(vt_param)
664
665
        return tostring(vt_params_xml).decode('utf-8')
666
667
    @staticmethod
668
    def get_refs_vt_as_xml_str(vt_id: str, vt_refs: Dict) -> str:
669
        """Return an xml element with references formatted as string.
670
        Arguments:
671
            vt_id: VT OID. Only used for logging in error case.
672
            vt_refs: Dictionary with the VT references.
673
        Return:
674
            Xml element as string.
675
        """
676
        vt_refs_xml = Element('refs')
677
        for ref_type, ref_values in vt_refs.items():
678
            for value in ref_values:
679
                vt_ref = Element('ref')
680
                if ref_type == "xref" and value:
681
                    for xref in value.split(', '):
682
                        try:
683
                            _type, _id = xref.split(':', 1)
684
                        except ValueError as e:
685
                            logger.error(
686
                                'Not possible to parse xref "%s" for VT %s: %s',
687
                                xref,
688
                                vt_id,
689
                                e,
690
                            )
691
                            continue
692
                        vt_ref.set('type', _type.lower())
693
                        vt_ref.set('id', _id)
694
                elif value:
695
                    vt_ref.set('type', ref_type.lower())
696
                    vt_ref.set('id', value)
697
                else:
698
                    continue
699
                vt_refs_xml.append(vt_ref)
700
701
        return tostring(vt_refs_xml).decode('utf-8')
702
703
    @staticmethod
704
    def get_dependencies_vt_as_xml_str(
705
        vt_id: str, vt_dependencies: List
706
    ) -> str:
707
        """Return  an xml element with dependencies as string.
708
        Arguments:
709
            vt_id: VT OID. Only used for logging in error case.
710
            vt_dependencies: List with the VT dependencies.
711
        Return:
712
            Xml element as string.
713
        """
714
        vt_deps_xml = Element('dependencies')
715
        for dep in vt_dependencies:
716
            _vt_dep = Element('dependency')
717
            if VT_BASE_OID in dep:
718
                _vt_dep.set('vt_id', dep)
719
            else:
720
                logger.error(
721
                    'Not possible to add dependency %s for VT %s', dep, vt_id
722
                )
723
                continue
724
            vt_deps_xml.append(_vt_dep)
725
726
        return tostring(vt_deps_xml).decode('utf-8')
727
728
    @staticmethod
729
    def get_creation_time_vt_as_xml_str(
730
        vt_id: str, vt_creation_time: str
731
    ) -> str:
732
        """Return creation time as string.
733
        Arguments:
734
            vt_id: VT OID. Only used for logging in error case.
735
            vt_creation_time: String with the VT creation time.
736
        Return:
737
           Xml element as string.
738
        """
739
        _time = Element('creation_time')
740
        try:
741
            _time.text = vt_creation_time
742
        except ValueError as e:
743
            logger.warning(
744
                "Not possible to parse creation time for VT %s: %s", vt_id, e
745
            )
746
        return tostring(_time).decode('utf-8')
747
748
    @staticmethod
749
    def get_modification_time_vt_as_xml_str(
750
        vt_id: str, vt_modification_time: str
751
    ) -> str:
752
        """Return modification time as string.
753
        Arguments:
754
            vt_id: VT OID. Only used for logging in error case.
755
            vt_modification_time: String with the VT modification time.
756
        Return:
757
            Xml element as string.
758
        """
759
        _time = Element('modification_time')
760
        try:
761
            _time.text = vt_modification_time
762
        except ValueError as e:
763
            logger.warning(
764
                "Not possible to parse modification time for VT %s: %s",
765
                vt_id,
766
                e,
767
            )
768
        return tostring(_time).decode('utf-8')
769
770
    @staticmethod
771
    def get_summary_vt_as_xml_str(vt_id: str, summary: str) -> str:
772
        """Return summary as string.
773
        Arguments:
774
            vt_id: VT OID. Only used for logging in error case.
775
            summary: String with a VT summary.
776
        Return:
777
            Xml element as string.
778
        """
779
        _summary = Element('summary')
780
        try:
781
            _summary.text = summary
782
        except ValueError as e:
783
            logger.warning(
784
                "Not possible to parse summary tag for VT %s: %s", vt_id, e
785
            )
786
        return tostring(_summary).decode('utf-8')
787
788
    @staticmethod
789
    def get_impact_vt_as_xml_str(vt_id: str, impact) -> str:
790
        """Return impact as string.
791
792
        Arguments:
793
            vt_id (str): VT OID. Only used for logging in error case.
794
            impact (str): String which explain the vulneravility impact.
795
        Return:
796
            string: xml element as string.
797
        """
798
        _impact = Element('impact')
799
        try:
800
            _impact.text = impact
801
        except ValueError as e:
802
            logger.warning(
803
                "Not possible to parse impact tag for VT %s: %s", vt_id, e
804
            )
805
        return tostring(_impact).decode('utf-8')
806
807
    @staticmethod
808
    def get_affected_vt_as_xml_str(vt_id: str, affected: str) -> str:
809
        """Return affected as string.
810
        Arguments:
811
            vt_id: VT OID. Only used for logging in error case.
812
            affected: String which explain what is affected.
813
        Return:
814
            Xml element as string.
815
        """
816
        _affected = Element('affected')
817
        try:
818
            _affected.text = affected
819
        except ValueError as e:
820
            logger.warning(
821
                "Not possible to parse affected tag for VT %s: %s", vt_id, e
822
            )
823
        return tostring(_affected).decode('utf-8')
824
825
    @staticmethod
826
    def get_insight_vt_as_xml_str(vt_id: str, insight: str) -> str:
827
        """Return insight as string.
828
        Arguments:
829
            vt_id: VT OID. Only used for logging in error case.
830
            insight: String giving an insight of the vulnerability.
831
        Return:
832
            Xml element as string.
833
        """
834
        _insight = Element('insight')
835
        try:
836
            _insight.text = insight
837
        except ValueError as e:
838
            logger.warning(
839
                "Not possible to parse insight tag for VT %s: %s", vt_id, e
840
            )
841
        return tostring(_insight).decode('utf-8')
842
843
    @staticmethod
844
    def get_solution_vt_as_xml_str(
845
        vt_id: str,
846
        solution: str,
847
        solution_type: Optional[str] = None,
848
        solution_method: Optional[str] = None,
849
    ) -> str:
850
        """Return solution as string.
851
        Arguments:
852
            vt_id: VT OID. Only used for logging in error case.
853
            solution: String giving a possible solution.
854
            solution_type: A solution type
855
            solution_method: A solution method
856
        Return:
857
            Xml element as string.
858
        """
859
        _solution = Element('solution')
860
        try:
861
            _solution.text = solution
862
        except ValueError as e:
863
            logger.warning(
864
                "Not possible to parse solution tag for VT %s: %s", vt_id, e
865
            )
866
        if solution_type:
867
            _solution.set('type', solution_type)
868
        if solution_method:
869
            _solution.set('method', solution_method)
870
        return tostring(_solution).decode('utf-8')
871
872
    @staticmethod
873
    def get_detection_vt_as_xml_str(
874
        vt_id: str,
875
        detection: Optional[str] = None,
876
        qod_type: Optional[str] = None,
877
        qod: Optional[str] = None,
878
    ) -> str:
879
        """Return detection as string.
880
        Arguments:
881
            vt_id: VT OID. Only used for logging in error case.
882
            detection: String which explain how the vulnerability
883
              was detected.
884
            qod_type: qod type.
885
            qod: qod value.
886
        Return:
887
            Xml element as string.
888
        """
889
        _detection = Element('detection')
890
        if detection:
891
            try:
892
                _detection.text = detection
893
            except ValueError as e:
894
                logger.warning(
895
                    "Not possible to parse detection tag for VT %s: %s",
896
                    vt_id,
897
                    e,
898
                )
899
        if qod_type:
900
            _detection.set('qod_type', qod_type)
901
        elif qod:
902
            _detection.set('qod', qod)
903
904
        return tostring(_detection).decode('utf-8')
905
906
    @property
907
    def is_running_as_root(self) -> bool:
908
        """Check if it is running as root user."""
909
        if self._is_running_as_root is not None:
910
            return self._is_running_as_root
911
912
        self._is_running_as_root = False
913
        if geteuid() == 0:
914
            self._is_running_as_root = True
915
916
        return self._is_running_as_root
917
918
    @property
919
    def sudo_available(self) -> bool:
920
        """Checks that sudo is available"""
921
        if self._sudo_available is not None:
922
            return self._sudo_available
923
924
        if self.is_running_as_root:
925
            self._sudo_available = False
926
            return self._sudo_available
927
928
        self._sudo_available = Openvas.check_sudo()
929
930
        return self._sudo_available
931
932
    def check(self) -> bool:
933
        """Checks that openvas command line tool is found and
934
        is executable."""
935
        has_openvas = Openvas.check()
936
        if not has_openvas:
937
            logger.error(
938
                'openvas executable not available. Please install openvas'
939
                ' into your PATH.'
940
            )
941
        return has_openvas
942
943
    def report_openvas_scan_status(self, kbdb: BaseDB, scan_id: str):
944
        """Get all status entries from redis kb.
945
946
        Arguments:
947
            kbdb: KB context where to get the status from.
948
            scan_id: Scan ID to identify the current scan.
949
        """
950
        all_status = kbdb.get_scan_status()
951
        all_hosts = dict()
952
        finished_hosts = list()
953
        for res in all_status:
954
            try:
955
                current_host, launched, total = res.split('/')
956
            except ValueError:
957
                continue
958
959
            try:
960
                if float(total) == 0:
961
                    continue
962
                elif float(total) == ScanProgress.DEAD_HOST:
963
                    host_prog = ScanProgress.DEAD_HOST
964
                else:
965
                    host_prog = int((float(launched) / float(total)) * 100)
966
            except TypeError:
967
                continue
968
969
            all_hosts[current_host] = host_prog
970
971
            if (
972
                host_prog == ScanProgress.DEAD_HOST
973
                or host_prog == ScanProgress.FINISHED
974
            ):
975
                finished_hosts.append(current_host)
976
977
            logger.debug(
978
                '%s: Host %s has progress: %d', scan_id, current_host, host_prog
979
            )
980
981
        self.set_scan_progress_batch(scan_id, host_progress=all_hosts)
982
983
        self.sort_host_finished(scan_id, finished_hosts)
984
985
    def report_openvas_results(self, db: BaseDB, scan_id: str) -> bool:
986
        """Get all result entries from redis kb."""
987
988
        vthelper = VtHelper(self.nvti)
989
990
        # Result messages come in the next form, with optional uri field
991
        # type ||| host ip ||| hostname ||| port ||| OID ||| value [|||uri]
992
        all_results = db.get_result()
993
        res_list = ResultList()
994
        total_dead = 0
995
        for res in all_results:
996
            if not res:
997
                continue
998
999
            msg = res.split('|||')
1000
            roid = msg[4].strip()
1001
            rqod = ''
1002
            rname = ''
1003
            current_host = msg[1].strip() if msg[1] else ''
1004
            rhostname = msg[2].strip() if msg[2] else ''
1005
            host_is_dead = "Host dead" in msg[5] or msg[0] == "DEADHOST"
1006
            host_deny = "Host access denied" in msg[5]
1007
            start_end_msg = msg[0] == "HOST_START" or msg[0] == "HOST_END"
1008
            host_count = msg[0] == "HOSTS_COUNT"
1009
            vt_aux = None
1010
1011
            # URI is optional and msg list length must be checked
1012
            ruri = ''
1013
            if len(msg) > 6:
1014
                ruri = msg[6]
1015
1016
            if (
1017
                roid
1018
                and not host_is_dead
1019
                and not host_deny
1020
                and not start_end_msg
1021
                and not host_count
1022
            ):
1023
                vt_aux = vthelper.get_single_vt(roid)
1024
1025
            if (
1026
                not vt_aux
1027
                and not host_is_dead
1028
                and not host_deny
1029
                and not start_end_msg
1030
                and not host_count
1031
            ):
1032
                logger.warning('Invalid VT oid %s for a result', roid)
1033
1034
            if vt_aux:
1035
                if vt_aux.get('qod_type'):
1036
                    qod_t = vt_aux.get('qod_type')
1037
                    rqod = self.nvti.QOD_TYPES[qod_t]
1038
                elif vt_aux.get('qod'):
1039
                    rqod = vt_aux.get('qod')
1040
1041
                rname = vt_aux.get('name')
1042
1043
            if msg[0] == 'ERRMSG':
1044
                res_list.add_scan_error_to_list(
1045
                    host=current_host,
1046
                    hostname=rhostname,
1047
                    name=rname,
1048
                    value=msg[5],
1049
                    port=msg[3],
1050
                    test_id=roid,
1051
                    uri=ruri,
1052
                )
1053
1054
            elif msg[0] == 'HOST_START' or msg[0] == 'HOST_END':
1055
                res_list.add_scan_log_to_list(
1056
                    host=current_host,
1057
                    name=msg[0],
1058
                    value=msg[5],
1059
                )
1060
1061
            elif msg[0] == 'LOG':
1062
                res_list.add_scan_log_to_list(
1063
                    host=current_host,
1064
                    hostname=rhostname,
1065
                    name=rname,
1066
                    value=msg[5],
1067
                    port=msg[3],
1068
                    qod=rqod,
1069
                    test_id=roid,
1070
                    uri=ruri,
1071
                )
1072
1073
            elif msg[0] == 'HOST_DETAIL':
1074
                res_list.add_scan_host_detail_to_list(
1075
                    host=current_host,
1076
                    hostname=rhostname,
1077
                    name=rname,
1078
                    value=msg[5],
1079
                    uri=ruri,
1080
                )
1081
1082
            elif msg[0] == 'ALARM':
1083
                rseverity = vthelper.get_severity_score(vt_aux)
1084
                res_list.add_scan_alarm_to_list(
1085
                    host=current_host,
1086
                    hostname=rhostname,
1087
                    name=rname,
1088
                    value=msg[5],
1089
                    port=msg[3],
1090
                    test_id=roid,
1091
                    severity=rseverity,
1092
                    qod=rqod,
1093
                    uri=ruri,
1094
                )
1095
1096
            # To process non-scanned dead hosts when
1097
            # test_alive_host_only in openvas is enable
1098
            elif msg[0] == 'DEADHOST':
1099
                try:
1100
                    total_dead = int(msg[5])
1101
                except TypeError:
1102
                    logger.debug('Error processing dead host count')
1103
1104
            # To update total host count
1105
            if msg[0] == 'HOSTS_COUNT':
1106
                try:
1107
                    count_total = int(msg[5])
1108
                    logger.debug(
1109
                        '%s: Set total hosts counted by OpenVAS: %d',
1110
                        scan_id,
1111
                        count_total,
1112
                    )
1113
                    self.set_scan_total_hosts(scan_id, count_total)
1114
                except TypeError:
1115
                    logger.debug('Error processing total host count')
1116
1117
        # Insert result batch into the scan collection table.
1118
        if len(res_list):
1119
            self.scan_collection.add_result_list(scan_id, res_list)
1120
            logger.debug(
1121
                '%s: Inserting %d results into scan collection table',
1122
                scan_id,
1123
                len(res_list),
1124
            )
1125
        if total_dead:
1126
            logger.debug(
1127
                '%s: Set dead hosts counted by OpenVAS: %d',
1128
                scan_id,
1129
                total_dead,
1130
            )
1131
            self.scan_collection.set_amount_dead_hosts(
1132
                scan_id, total_dead=total_dead
1133
            )
1134
1135
        return len(res_list) > 0
1136
1137
    @staticmethod
1138
    def is_openvas_process_alive(openvas_process: psutil.Popen) -> bool:
1139
1140
        if openvas_process.status() == psutil.STATUS_ZOMBIE:
1141
            logger.debug("Process is a Zombie, waiting for it to clean up")
1142
            openvas_process.wait()
1143
        return openvas_process.is_running()
1144
1145
    def stop_scan_cleanup(
1146
        self,
1147
        kbdb: BaseDB,
1148
        scan_id: str,
1149
        ovas_process: psutil.Popen,  # pylint: disable=arguments-differ
1150
    ):
1151
        """Set a key in redis to indicate the wrapper is stopped.
1152
        It is done through redis because it is a new multiprocess
1153
        instance and it is not possible to reach the variables
1154
        of the grandchild process.
1155
        Indirectly sends SIGUSR1 to the running openvas scan process
1156
        via an invocation of openvas with the --scan-stop option to
1157
        stop it."""
1158
1159
        if kbdb:
1160
            # Set stop flag in redis
1161
            kbdb.stop_scan(scan_id)
1162
1163
            # Check if openvas is running
1164
            if ovas_process.is_running():
1165
                # Cleaning in case of Zombie Process
1166
                if ovas_process.status() == psutil.STATUS_ZOMBIE:
1167
                    logger.debug(
1168
                        '%s: Process with PID %s is a Zombie process.'
1169
                        ' Cleaning up...',
1170
                        scan_id,
1171
                        ovas_process.pid,
1172
                    )
1173
                    ovas_process.wait()
1174
                # Stop openvas process and wait until it stopped
1175
                else:
1176
                    can_stop_scan = Openvas.stop_scan(
1177
                        scan_id,
1178
                        not self.is_running_as_root and self.sudo_available,
1179
                    )
1180
                    if not can_stop_scan:
1181
                        logger.debug(
1182
                            'Not possible to stop scan process: %s.',
1183
                            ovas_process,
1184
                        )
1185
                        return
1186
1187
                    logger.debug('Stopping process: %s', ovas_process)
1188
1189
                    while ovas_process.is_running():
1190
                        if ovas_process.status() == psutil.STATUS_ZOMBIE:
1191
                            ovas_process.wait()
1192
                        else:
1193
                            time.sleep(0.1)
1194
            else:
1195
                logger.debug(
1196
                    "%s: Process with PID %s already stopped",
1197
                    scan_id,
1198
                    ovas_process.pid,
1199
                )
1200
1201
            # Clean redis db
1202
            for scan_db in kbdb.get_scan_databases():
1203
                self.main_db.release_database(scan_db)
1204
1205
    def exec_scan(self, scan_id: str):
1206
        """Starts the OpenVAS scanner for scan_id scan."""
1207
        params = self.scan_collection.get_options(scan_id)
1208
        if params.get("dry_run"):
1209
            dryrun = DryRun(self)
1210
            dryrun.exec_dry_run_scan(scan_id, self.nvti, OSPD_PARAMS)
1211
            return
1212
1213
        do_not_launch = False
1214
        kbdb = self.main_db.get_new_kb_database()
1215
        scan_prefs = PreferenceHandler(
1216
            scan_id, kbdb, self.scan_collection, self.nvti
1217
        )
1218
        kbdb.add_scan_id(scan_id)
1219
        scan_prefs.prepare_target_for_openvas()
1220
1221
        if not scan_prefs.prepare_ports_for_openvas():
1222
            self.add_scan_error(
1223
                scan_id, name='', host='', value='Invalid port list.'
1224
            )
1225
            do_not_launch = True
1226
1227
        # Set credentials
1228
        if not scan_prefs.prepare_credentials_for_openvas():
1229
            error = (
1230
                'All authentifications contain errors.'
1231
                + 'Starting unauthenticated scan instead.'
1232
            )
1233
            self.add_scan_error(
1234
                scan_id,
1235
                name='',
1236
                host='',
1237
                value=error,
1238
            )
1239
            logger.error(error)
1240
        errors = scan_prefs.get_error_messages()
1241
        for e in errors:
1242
            error = 'Malformed credential. ' + e
1243
            self.add_scan_error(
1244
                scan_id,
1245
                name='',
1246
                host='',
1247
                value=error,
1248
            )
1249
            logger.error(error)
1250
1251
        if not scan_prefs.prepare_plugins_for_openvas():
1252
            self.add_scan_error(
1253
                scan_id, name='', host='', value='No VTS to run.'
1254
            )
1255
            do_not_launch = True
1256
1257
        scan_prefs.prepare_main_kbindex_for_openvas()
1258
        scan_prefs.prepare_host_options_for_openvas()
1259
        scan_prefs.prepare_scan_params_for_openvas(OSPD_PARAMS)
1260
        scan_prefs.prepare_reverse_lookup_opt_for_openvas()
1261
        scan_prefs.prepare_alive_test_option_for_openvas()
1262
1263
        # VT preferences are stored after all preferences have been processed,
1264
        # since alive tests preferences have to be able to overwrite default
1265
        # preferences of ping_host.nasl for the classic method.
1266
        scan_prefs.prepare_nvt_preferences()
1267
        scan_prefs.prepare_boreas_alive_test()
1268
1269
        # Release memory used for scan preferences.
1270
        del scan_prefs
1271
1272
        if do_not_launch or kbdb.scan_is_stopped(scan_id):
1273
            self.main_db.release_database(kbdb)
1274
            return
1275
1276
        openvas_process = Openvas.start_scan(
1277
            scan_id,
1278
            not self.is_running_as_root and self.sudo_available,
1279
            self._niceness,
1280
        )
1281
1282
        if openvas_process is None:
1283
            self.main_db.release_database(kbdb)
1284
            return
1285
1286
        kbdb.add_scan_process_id(openvas_process.pid)
1287
        logger.debug('pid = %s', openvas_process.pid)
1288
1289
        # Wait until the scanner starts and loads all the preferences.
1290
        while kbdb.get_status(scan_id) == 'new':
1291
            res = openvas_process.poll()
1292
            if res and res < 0:
1293
                self.stop_scan_cleanup(kbdb, scan_id, openvas_process)
1294
                logger.error(
1295
                    'It was not possible run the task %s, since openvas ended '
1296
                    'unexpectedly with errors during launching.',
1297
                    scan_id,
1298
                )
1299
                return
1300
1301
            time.sleep(1)
1302
1303
        got_results = False
1304
        while True:
1305
1306
            openvas_process_is_alive = self.is_openvas_process_alive(
1307
                openvas_process
1308
            )
1309
            target_is_finished = kbdb.target_is_finished(scan_id)
1310
            scan_stopped = self.get_scan_status(scan_id) == ScanStatus.STOPPED
1311
1312
            # Report new Results and update status
1313
            got_results = self.report_openvas_results(kbdb, scan_id)
1314
            self.report_openvas_scan_status(kbdb, scan_id)
1315
1316
            # Check if the client stopped the whole scan
1317
            if scan_stopped:
1318
                logger.debug('%s: Scan stopped by the client', scan_id)
1319
1320
                self.stop_scan_cleanup(kbdb, scan_id, openvas_process)
1321
1322
                # clean main_db, but wait for scanner to finish.
1323
                while not kbdb.target_is_finished(scan_id):
1324
                    logger.debug('%s: Waiting for openvas to finish', scan_id)
1325
                    time.sleep(1)
1326
                self.main_db.release_database(kbdb)
1327
                return
1328
1329
            # Scan end. No kb in use for this scan id
1330
            if target_is_finished:
1331
                logger.debug('%s: Target is finished', scan_id)
1332
                break
1333
1334
            if not openvas_process_is_alive:
1335
                logger.error(
1336
                    'Task %s was unexpectedly stopped or killed.',
1337
                    scan_id,
1338
                )
1339
                self.add_scan_error(
1340
                    scan_id,
1341
                    name='',
1342
                    host='',
1343
                    value='Task was unexpectedly stopped or killed.',
1344
                )
1345
1346
                # check for scanner error messages before leaving.
1347
                self.report_openvas_results(kbdb, scan_id)
1348
1349
                kbdb.stop_scan(scan_id)
1350
1351
                for scan_db in kbdb.get_scan_databases():
1352
                    self.main_db.release_database(scan_db)
1353
                self.main_db.release_database(kbdb)
1354
                return
1355
1356
            # Wait a second before trying to get result from redis if there
1357
            # was no results before.
1358
            # Otherwise, wait 50 msec to give access other process to redis.
1359
            if not got_results:
1360
                time.sleep(1)
1361
            else:
1362
                time.sleep(0.05)
1363
            got_results = False
1364
1365
        # Delete keys from KB related to this scan task.
1366
        logger.debug('%s: End Target. Release main database', scan_id)
1367
        self.main_db.release_database(kbdb)
1368
1369
1370
def main():
1371
    """OSP openvas main function."""
1372
    daemon_main('OSPD - openvas', OSPDopenvas)
1373
1374
1375
if __name__ == '__main__':
1376
    main()
1377