Completed
Push — master ( 351cbc...3c6238 )
by
unknown
37s queued 13s
created

ospd_openvas.daemon.OSPDopenvas.update_progress()   A

Complexity

Conditions 5

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nop 4
dl 0
loc 25
rs 9.1832
c 0
b 0
f 0
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.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
        'description': 'Automatically enable the plugins that are depended on',
81
    },
82
    'cgi_path': {
83
        'type': 'string',
84
        'name': 'cgi_path',
85
        'default': '/cgi-bin:/scripts',
86
        'mandatory': 1,
87
        'description': 'Look for default CGIs in /cgi-bin and /scripts',
88
    },
89
    'checks_read_timeout': {
90
        'type': 'integer',
91
        'name': 'checks_read_timeout',
92
        'default': 5,
93
        'mandatory': 1,
94
        'description': (
95
            'Number  of seconds that the security checks will '
96
            + 'wait for when doing a recv()'
97
        ),
98
    },
99
    'drop_privileges': {
100
        'type': 'boolean',
101
        'name': 'drop_privileges',
102
        'default': 0,
103
        'mandatory': 1,
104
        'description': '',
105
    },
106
    'non_simult_ports': {
107
        'type': 'string',
108
        'name': 'non_simult_ports',
109
        'default': '139, 445, 3389, Services/irc',
110
        'mandatory': 1,
111
        'description': (
112
            'Prevent to make two connections on the same given '
113
            + 'ports at the same time.'
114
        ),
115
    },
116
    'open_sock_max_attempts': {
117
        'type': 'integer',
118
        'name': 'open_sock_max_attempts',
119
        'default': 5,
120
        'mandatory': 0,
121
        'description': (
122
            'Number of unsuccessful retries to open the socket '
123
            + 'before to set the port as closed.'
124
        ),
125
    },
126
    'timeout_retry': {
127
        'type': 'integer',
128
        'name': 'timeout_retry',
129
        'default': 5,
130
        'mandatory': 0,
131
        'description': (
132
            'Number of retries when a socket connection attempt ' + 'timesout.'
133
        ),
134
    },
135
    'optimize_test': {
136
        'type': 'integer',
137
        'name': 'optimize_test',
138
        'default': 5,
139
        'mandatory': 0,
140
        'description': (
141
            'By default, openvas does not trust the remote ' + 'host banners.'
142
        ),
143
    },
144
    'plugins_timeout': {
145
        'type': 'integer',
146
        'name': 'plugins_timeout',
147
        'default': 5,
148
        'mandatory': 0,
149
        'description': 'This is the maximum lifetime, in seconds of a plugin.',
150
    },
151
    'report_host_details': {
152
        'type': 'boolean',
153
        'name': 'report_host_details',
154
        'default': 1,
155
        'mandatory': 1,
156
        'description': '',
157
    },
158
    'safe_checks': {
159
        'type': 'boolean',
160
        'name': 'safe_checks',
161
        'default': 1,
162
        'mandatory': 1,
163
        'description': (
164
            'Disable the plugins with potential to crash '
165
            + 'the remote services'
166
        ),
167
    },
168
    'scanner_plugins_timeout': {
169
        'type': 'integer',
170
        'name': 'scanner_plugins_timeout',
171
        'default': 36000,
172
        'mandatory': 1,
173
        'description': 'Like plugins_timeout, but for ACT_SCANNER plugins.',
174
    },
175
    'time_between_request': {
176
        'type': 'integer',
177
        'name': 'time_between_request',
178
        'default': 0,
179
        'mandatory': 0,
180
        'description': (
181
            'Allow to set a wait time between two actions '
182
            + '(open, send, close).'
183
        ),
184
    },
185
    'unscanned_closed': {
186
        'type': 'boolean',
187
        'name': 'unscanned_closed',
188
        'default': 1,
189
        'mandatory': 1,
190
        'description': '',
191
    },
192
    'unscanned_closed_udp': {
193
        'type': 'boolean',
194
        'name': 'unscanned_closed_udp',
195
        'default': 1,
196
        'mandatory': 1,
197
        'description': '',
198
    },
199
    'expand_vhosts': {
200
        'type': 'boolean',
201
        'name': 'expand_vhosts',
202
        'default': 1,
203
        'mandatory': 0,
204
        'description': 'Whether to expand the target hosts '
205
        + 'list of vhosts with values gathered from sources '
206
        + 'such as reverse-lookup queries and VT checks '
207
        + 'for SSL/TLS certificates.',
208
    },
209
    'test_empty_vhost': {
210
        'type': 'boolean',
211
        'name': 'test_empty_vhost',
212
        'default': 0,
213
        'mandatory': 0,
214
        'description': 'If  set  to  yes, the scanner will '
215
        + 'also test the target by using empty vhost value '
216
        + 'in addition to the targets associated vhost values.',
217
    },
218
}
219
220
221
def safe_int(value: str) -> Optional[int]:
222
    """ Convert a string into an integer and return None in case of errors
223
    during conversion
224
    """
225
    try:
226
        return int(value)
227
    except (ValueError, TypeError):
228
        return None
229
230
231
class OpenVasVtsFilter(VtsFilter):
232
233
    """ Methods to overwrite the ones in the original class.
234
    """
235
236
    def __init__(self, nvticache: NVTICache) -> None:
237
        super().__init__()
238
239
        self.nvti = nvticache
240
241
    def format_vt_modification_time(self, value: str) -> str:
242
        """ Convert the string seconds since epoch into a 19 character
243
        string representing YearMonthDayHourMinuteSecond,
244
        e.g. 20190319122532. This always refers to UTC.
245
        """
246
247
        return datetime.utcfromtimestamp(int(value)).strftime("%Y%m%d%H%M%S")
248
249
    def get_filtered_vts_list(self, vts, vt_filter: str) -> Optional[List[str]]:
250
        """ Gets a collection of vulnerability test from the redis cache,
251
        which match the filter.
252
253
        Arguments:
254
            vt_filter: Filter to apply to the vts collection.
255
            vts: The complete vts collection.
256
257
        Returns:
258
            List with filtered vulnerability tests. The list can be empty.
259
            None in case of filter parse failure.
260
        """
261
        filters = self.parse_filters(vt_filter)
262
        if not filters:
263
            return None
264
265
        if not self.nvti:
266
            return None
267
268
        vt_oid_list = [vtlist[1] for vtlist in self.nvti.get_oids()]
269
        vt_oid_list_temp = copy.copy(vt_oid_list)
270
        vthelper = VtHelper(self.nvti)
271
272
        for element, oper, filter_val in filters:
273
            for vt_oid in vt_oid_list_temp:
274
                if vt_oid not in vt_oid_list:
275
                    continue
276
277
                vt = vthelper.get_single_vt(vt_oid)
278
                if vt is None or not vt.get(element):
279
                    vt_oid_list.remove(vt_oid)
280
                    continue
281
282
                elem_val = vt.get(element)
283
                val = self.format_filter_value(element, elem_val)
284
285
                if self.filter_operator[oper](val, filter_val):
286
                    continue
287
                else:
288
                    vt_oid_list.remove(vt_oid)
289
290
        return vt_oid_list
291
292
293
class OSPDopenvas(OSPDaemon):
294
295
    """ Class for ospd-openvas daemon. """
296
297
    def __init__(
298
        self, *, niceness=None, lock_file_dir='/var/run/ospd', **kwargs
299
    ):
300
        """ Initializes the ospd-openvas daemon's internal data. """
301
        self.main_db = MainDB()
302
        self.nvti = NVTICache(self.main_db)
303
304
        super().__init__(
305
            customvtfilter=OpenVasVtsFilter(self.nvti),
306
            storage=dict,
307
            file_storage_dir=lock_file_dir,
308
            **kwargs,
309
        )
310
311
        self.server_version = __version__
312
313
        self._niceness = str(niceness)
314
315
        self.feed_lock = LockFile(Path(lock_file_dir) / 'feed-update.lock')
316
        self.daemon_info['name'] = 'OSPd OpenVAS'
317
        self.scanner_info['name'] = 'openvas'
318
        self.scanner_info['version'] = ''  # achieved during self.init()
319
        self.scanner_info['description'] = OSPD_DESC
320
321
        for name, param in OSPD_PARAMS.items():
322
            self.set_scanner_param(name, param)
323
324
        self._sudo_available = None
325
        self._is_running_as_root = None
326
327
        self.scan_only_params = dict()
328
329
    def init(self, server: BaseServer) -> None:
330
331
        self.scan_collection.init()
332
333
        server.start(self.handle_client_stream)
334
335
        self.scanner_info['version'] = Openvas.get_version()
336
337
        self.set_params_from_openvas_settings()
338
339
        with self.feed_lock.wait_for_lock():
340
            Openvas.load_vts_into_redis()
341
            current_feed = self.nvti.get_feed_version()
342
            self.set_vts_version(vts_version=current_feed)
343
344
        vthelper = VtHelper(self.nvti)
345
        self.vts.sha256_hash = vthelper.calculate_vts_collection_hash()
346
347
        self.initialized = True
348
349
    def set_params_from_openvas_settings(self):
350
        """ Set OSPD_PARAMS with the params taken from the openvas executable.
351
        """
352
        param_list = Openvas.get_settings()
353
354
        for elem in param_list:
355
            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...
356
                self.scan_only_params[elem] = param_list[elem]
357
            else:
358
                OSPD_PARAMS[elem]['default'] = param_list[elem]
359
360
    def feed_is_outdated(self, current_feed: str) -> Optional[bool]:
361
        """ Compare the current feed with the one in the disk.
362
363
        Return:
364
            False if there is no new feed.
365
            True if the feed version in disk is newer than the feed in
366
                redis cache.
367
            None if there is no feed on the disk.
368
        """
369
        plugins_folder = self.scan_only_params.get('plugins_folder')
370
        if not plugins_folder:
371
            raise OspdOpenvasError("Error: Path to plugins folder not found.")
372
373
        feed_info_file = Path(plugins_folder) / 'plugin_feed_info.inc'
374
        if not feed_info_file.exists():
375
            self.set_params_from_openvas_settings()
376
            logger.debug('Plugins feed file %s not found.', feed_info_file)
377
            return None
378
379
        current_feed = safe_int(current_feed)
380
381
        feed_date = None
382
        with feed_info_file.open() as fcontent:
383
            for line in fcontent:
384
                if "PLUGIN_SET" in line:
385
                    feed_date = line.split('=', 1)[1]
386
                    feed_date = feed_date.strip()
387
                    feed_date = feed_date.replace(';', '')
388
                    feed_date = feed_date.replace('"', '')
389
                    feed_date = safe_int(feed_date)
390
                    break
391
392
        logger.debug("Current feed version: %s", current_feed)
393
        logger.debug("Plugin feed version: %s", feed_date)
394
395
        return (
396
            (not feed_date) or (not current_feed) or (current_feed < feed_date)
397
        )
398
399
    def check_feed(self):
400
        """ Check if there is a feed update.
401
402
        Wait until all the running scans finished. Set a flag to announce there
403
        is a pending feed update, which avoids to start a new scan.
404
        """
405
        if not self.vts.is_cache_available:
406
            return
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 report_openvas_scan_status(self, kbdb: BaseDB, scan_id: str):
800
        """ Get all status entries from redis kb.
801
802
        Arguments:
803
            scan_id: Scan ID to identify the current scan.
804
            current_host: Host to be updated.
805
        """
806
        all_status = kbdb.get_scan_status()
807
        all_hosts = dict()
808
        finished_hosts = list()
809
        for res in all_status:
810
            try:
811
                current_host, launched, total = res.split('/')
812
            except ValueError:
813
                continue
814
815
            try:
816
                if float(total) == 0:
817
                    continue
818
                elif float(total) == ScanProgress.DEAD_HOST:
819
                    host_prog = ScanProgress.DEAD_HOST
820
                else:
821
                    host_prog = int((float(launched) / float(total)) * 100)
822
            except TypeError:
823
                continue
824
825
            all_hosts[current_host] = host_prog
826
827
            if (
828
                host_prog == ScanProgress.DEAD_HOST
829
                or host_prog == ScanProgress.FINISHED
830
            ):
831
                finished_hosts.append(current_host)
832
833
        self.set_scan_progress_batch(scan_id, host_progress=all_hosts)
834
835
        self.sort_host_finished(scan_id, finished_hosts)
836
837
    def get_severity_score(self, vt_aux: dict) -> Optional[float]:
838
        """ Return the severity score for the given oid.
839
        Arguments:
840
            vt_aux: VT element from which to get the severity vector
841
        Returns:
842
            The calculated cvss base value. None if there is no severity
843
            vector or severity type is not cvss base version 2.
844
        """
845
        if vt_aux:
846
            severity_type = vt_aux['severities'].get('severity_type')
847
            severity_vector = vt_aux['severities'].get('severity_base_vector')
848
849
            if severity_type == "cvss_base_v2" and severity_vector:
850
                return CVSS.cvss_base_v2_value(severity_vector)
851
852
        return None
853
854
    def report_openvas_results(self, db: BaseDB, scan_id: str) -> bool:
855
        """ Get all result entries from redis kb. """
856
857
        vthelper = VtHelper(self.nvti)
858
859
        # Result messages come in the next form, with optional uri field
860
        # type ||| host ip ||| hostname ||| port ||| OID ||| value [|||uri]
861
        all_results = db.get_result()
862
        res_list = ResultList()
863
        total_dead = 0
864
        for res in all_results:
865
            if not res:
866
                continue
867
868
            msg = res.split('|||')
869
            roid = msg[4].strip()
870
            rqod = ''
871
            rname = ''
872
            current_host = msg[1].strip() if msg[1] else ''
873
            rhostname = msg[2].strip() if msg[2] else ''
874
            host_is_dead = "Host dead" in msg[5] or msg[0] == "DEADHOST"
875
            host_deny = "Host access denied" in msg[5]
876
            start_end_msg = msg[0] == "HOST_START" or msg[0] == "HOST_END"
877
            vt_aux = None
878
879
            # URI is optional and msg list length must be checked
880
            ruri = ''
881
            if len(msg) > 6:
882
                ruri = msg[6]
883
884
            if (
885
                roid
886
                and not host_is_dead
887
                and not host_deny
888
                and not start_end_msg
889
            ):
890
                vt_aux = vthelper.get_single_vt(roid)
891
892
            if (
893
                not vt_aux
894
                and not host_is_dead
895
                and not host_deny
896
                and not start_end_msg
897
            ):
898
                logger.warning('Invalid VT oid %s for a result', roid)
899
900
            if vt_aux:
901
                if vt_aux.get('qod_type'):
902
                    qod_t = vt_aux.get('qod_type')
903
                    rqod = self.nvti.QOD_TYPES[qod_t]
904
                elif vt_aux.get('qod'):
905
                    rqod = vt_aux.get('qod')
906
907
                rname = vt_aux.get('name')
908
909
            if msg[0] == 'ERRMSG':
910
                res_list.add_scan_error_to_list(
911
                    host=current_host,
912
                    hostname=rhostname,
913
                    name=rname,
914
                    value=msg[5],
915
                    port=msg[3],
916
                    test_id=roid,
917
                    uri=ruri,
918
                )
919
920
            elif msg[0] == 'HOST_START' or msg[0] == 'HOST_END':
921
                res_list.add_scan_log_to_list(
922
                    host=current_host, name=msg[0], value=msg[5],
923
                )
924
925
            elif msg[0] == 'LOG':
926
                res_list.add_scan_log_to_list(
927
                    host=current_host,
928
                    hostname=rhostname,
929
                    name=rname,
930
                    value=msg[5],
931
                    port=msg[3],
932
                    qod=rqod,
933
                    test_id=roid,
934
                    uri=ruri,
935
                )
936
937
            elif msg[0] == 'HOST_DETAIL':
938
                res_list.add_scan_host_detail_to_list(
939
                    host=current_host,
940
                    hostname=rhostname,
941
                    name=rname,
942
                    value=msg[5],
943
                    uri=ruri,
944
                )
945
946
            elif msg[0] == 'ALARM':
947
                rseverity = self.get_severity_score(vt_aux)
948
                res_list.add_scan_alarm_to_list(
949
                    host=current_host,
950
                    hostname=rhostname,
951
                    name=rname,
952
                    value=msg[5],
953
                    port=msg[3],
954
                    test_id=roid,
955
                    severity=rseverity,
956
                    qod=rqod,
957
                    uri=ruri,
958
                )
959
960
            # To process non-scanned dead hosts when
961
            # test_alive_host_only in openvas is enable
962
            elif msg[0] == 'DEADHOST':
963
                try:
964
                    total_dead = int(msg[5])
965
                except TypeError:
966
                    logger.debug('Error processing dead host count')
967
968
        # Insert result batch into the scan collection table.
969
        if len(res_list):
970
            self.scan_collection.add_result_list(scan_id, res_list)
971
972
        if total_dead:
973
            self.scan_collection.set_amount_dead_hosts(
974
                scan_id, total_dead=total_dead
975
            )
976
977
        return len(res_list) > 0
978
979
    def is_openvas_process_alive(
980
        self, kbdb: BaseDB, ovas_pid: str, openvas_scan_id: str
981
    ) -> bool:
982
        parent_exists = True
983
        parent = None
984
        try:
985
            parent = psutil.Process(int(ovas_pid))
986
        except psutil.NoSuchProcess:
987
            logger.debug('Process with pid %s already stopped', ovas_pid)
988
            parent_exists = False
989
        except TypeError:
990
            logger.debug(
991
                'Scan with ID %s never started and stopped unexpectedly',
992
                openvas_scan_id,
993
            )
994
            parent_exists = False
995
996
        is_zombie = False
997
        if parent and parent.status() == psutil.STATUS_ZOMBIE:
998
            is_zombie = True
999
1000
        if (not parent_exists or is_zombie) and kbdb:
1001
            if kbdb and kbdb.scan_is_stopped(openvas_scan_id):
1002
                return True
1003
            return False
1004
1005
        return True
1006
1007
    def stop_scan_cleanup(  # pylint: disable=arguments-differ
1008
        self, global_scan_id: str
1009
    ):
1010
        """ Set a key in redis to indicate the wrapper is stopped.
1011
        It is done through redis because it is a new multiprocess
1012
        instance and it is not possible to reach the variables
1013
        of the grandchild process. Send SIGUSR2 to openvas to stop
1014
        each running scan."""
1015
1016
        openvas_scan_id, kbdb = self.main_db.find_kb_database_by_scan_id(
1017
            global_scan_id
1018
        )
1019
        if kbdb:
1020
            kbdb.stop_scan(openvas_scan_id)
1021
            ovas_pid = kbdb.get_scan_process_id()
1022
1023
            parent = None
1024
            try:
1025
                parent = psutil.Process(int(ovas_pid))
1026
            except psutil.NoSuchProcess:
1027
                logger.debug('Process with pid %s already stopped', ovas_pid)
1028
            except TypeError:
1029
                logger.debug(
1030
                    'Scan with ID %s never started and stopped unexpectedly',
1031
                    openvas_scan_id,
1032
                )
1033
1034
            if parent:
1035
                can_stop_scan = Openvas.stop_scan(
1036
                    openvas_scan_id,
1037
                    not self.is_running_as_root and self.sudo_available,
1038
                )
1039
                if not can_stop_scan:
1040
                    logger.debug(
1041
                        'Not possible to stop scan process: %s.', parent,
1042
                    )
1043
                    return False
1044
1045
                logger.debug('Stopping process: %s', parent)
1046
1047
                while parent:
1048
                    try:
1049
                        parent = psutil.Process(int(ovas_pid))
1050
                    except psutil.NoSuchProcess:
1051
                        parent = None
1052
1053
            for scan_db in kbdb.get_scan_databases():
1054
                self.main_db.release_database(scan_db)
1055
1056
    def exec_scan(self, scan_id: str):
1057
        """ Starts the OpenVAS scanner for scan_id scan. """
1058
        do_not_launch = False
1059
        kbdb = self.main_db.get_new_kb_database()
1060
        scan_prefs = PreferenceHandler(
1061
            scan_id, kbdb, self.scan_collection, self.nvti
1062
        )
1063
        openvas_scan_id = scan_prefs.prepare_openvas_scan_id_for_openvas()
1064
        scan_prefs.prepare_target_for_openvas()
1065
1066
        if not scan_prefs.prepare_ports_for_openvas():
1067
            self.add_scan_error(
1068
                scan_id, name='', host='', value='No port list defined.'
1069
            )
1070
            do_not_launch = True
1071
1072
        # Set credentials
1073
        if not scan_prefs.prepare_credentials_for_openvas():
1074
            self.add_scan_error(
1075
                scan_id, name='', host='', value='Malformed credential.'
1076
            )
1077
            do_not_launch = True
1078
1079
        if not scan_prefs.prepare_plugins_for_openvas():
1080
            self.add_scan_error(
1081
                scan_id, name='', host='', value='No VTS to run.'
1082
            )
1083
            do_not_launch = True
1084
1085
        scan_prefs.prepare_main_kbindex_for_openvas()
1086
        scan_prefs.prepare_host_options_for_openvas()
1087
        scan_prefs.prepare_scan_params_for_openvas(OSPD_PARAMS)
1088
        scan_prefs.prepare_reverse_lookup_opt_for_openvas()
1089
        scan_prefs.prepare_alive_test_option_for_openvas()
1090
        scan_prefs.prepare_boreas_alive_test()
1091
1092
        # Release memory used for scan preferences.
1093
        del scan_prefs
1094
1095
        if do_not_launch:
1096
            self.main_db.release_database(kbdb)
1097
            return
1098
1099
        result = Openvas.start_scan(
1100
            openvas_scan_id,
1101
            not self.is_running_as_root and self.sudo_available,
1102
            self._niceness,
1103
        )
1104
1105
        if result is None:
1106
            self.main_db.release_database(kbdb)
1107
            return
1108
1109
        ovas_pid = result.pid
1110
        kbdb.add_scan_process_id(ovas_pid)
1111
        logger.debug('pid = %s', ovas_pid)
1112
1113
        # Wait until the scanner starts and loads all the preferences.
1114
        while kbdb.get_status(openvas_scan_id) == 'new':
1115
            res = result.poll()
1116
            if res and res < 0:
1117
                self.stop_scan_cleanup(scan_id)
1118
                logger.error(
1119
                    'It was not possible run the task %s, since openvas ended '
1120
                    'unexpectedly with errors during launching.',
1121
                    scan_id,
1122
                )
1123
                return
1124
1125
            time.sleep(1)
1126
1127
        while True:
1128
            if not kbdb.target_is_finished(
1129
                scan_id
1130
            ) and not self.is_openvas_process_alive(
1131
                kbdb, ovas_pid, openvas_scan_id
1132
            ):
1133
                logger.error(
1134
                    'Task %s was unexpectedly stopped or killed.', scan_id,
1135
                )
1136
                self.add_scan_error(
1137
                    scan_id,
1138
                    name='',
1139
                    host='',
1140
                    value='Task was unexpectedly stopped or killed.',
1141
                )
1142
                kbdb.stop_scan(openvas_scan_id)
1143
                for scan_db in kbdb.get_scan_databases():
1144
                    self.main_db.release_database(scan_db)
1145
                self.main_db.release_database(kbdb)
1146
                return
1147
1148
            # Wait a second before trying to get result from redis if there
1149
            # was no results before.
1150
            # Otherwise, wait 50 msec to give access other process to redis.
1151
            got_results = False
1152
            if not got_results:
1153
                time.sleep(1)
1154
            else:
1155
                time.sleep(0.05)
1156
1157
            # Check if the client stopped the whole scan
1158
            if kbdb.scan_is_stopped(openvas_scan_id):
1159
                # clean main_db, but wait for scanner to finish.
1160
                while not kbdb.target_is_finished(scan_id):
1161
                    time.sleep(1)
1162
                self.main_db.release_database(kbdb)
1163
                return
1164
1165
            got_results = self.report_openvas_results(kbdb, scan_id)
1166
            self.report_openvas_scan_status(kbdb, scan_id)
1167
1168
            # Scan end. No kb in use for this scan id
1169
            if kbdb.target_is_finished(scan_id):
1170
                break
1171
1172
        # Delete keys from KB related to this scan task.
1173
        self.main_db.release_database(kbdb)
1174
1175
1176
def main():
1177
    """ OSP openvas main function. """
1178
    daemon_main('OSPD - openvas', OSPDopenvas)
1179
1180
1181
if __name__ == '__main__':
1182
    main()
1183