Passed
Pull Request — master (#192)
by Juan José
01:36
created

OSPDopenvas.get_severities_vt_as_xml_str()   B

Complexity

Conditions 6

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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