Passed
Pull Request — master (#236)
by Juan José
01:49
created

ospd_openvas.daemon.OSPDopenvas.get_single_vt()   F

Complexity

Conditions 33

Size

Total Lines 105
Code Lines 89

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 33
eloc 89
nop 3
dl 0
loc 105
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ospd_openvas.daemon.OSPDopenvas.get_single_vt() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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