Passed
Pull Request — master (#422)
by
unknown
01:54
created

OSPDopenvas.report_openvas_results_redis()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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