Passed
Pull Request — master (#399)
by
unknown
01:29
created

ospd_openvas.daemon.OSPDopenvas.stop_scan_cleanup()   C

Complexity

Conditions 9

Size

Total Lines 49
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

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