Passed
Pull Request — master (#242)
by Juan José
02:03
created

ospd_openvas.daemon.OSPDopenvas.check_feed()   B

Complexity

Conditions 6

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 19
nop 1
dl 0
loc 33
rs 8.5166
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.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
        if not self.vts.is_cache_available:
409
            return
410
411
        current_feed = self.nvti.get_feed_version()
412
        is_outdated = self.feed_is_outdated(current_feed)
413
414
        # Check if the nvticache in redis is outdated
415
        if not current_feed or is_outdated:
416
            with self.feed_lock as fl:
417
                if fl.has_lock():
418
                    self.initialized = False
419
                    Openvas.load_vts_into_redis()
420
                    current_feed = self.nvti.get_feed_version()
421
                    self.set_vts_version(vts_version=current_feed)
422
423
                    vthelper = VtHelper(self.nvti)
424
                    self.vts.sha256_hash = (
425
                        vthelper.calculate_vts_collection_hash()
426
                    )
427
                    self.initialized = True
428
                else:
429
                    logger.debug(
430
                        "The feed was not upload or it is outdated, "
431
                        "but other process is locking the update. "
432
                        "Trying again later..."
433
                    )
434
                    return
435
436
    def scheduler(self):
437
        """This method is called periodically to run tasks."""
438
        self.check_feed()
439
440
    def get_vt_iterator(
441
        self, vt_selection: List[str] = None, details: bool = True
442
    ) -> Iterator[Tuple[str, Dict]]:
443
        vthelper = VtHelper(self.nvti)
444
        return vthelper.get_vt_iterator(vt_selection, details)
445
446
    @staticmethod
447
    def get_custom_vt_as_xml_str(vt_id: str, custom: Dict) -> str:
448
        """ Return an xml element with custom metadata formatted as string.
449
        Arguments:
450
            vt_id: VT OID. Only used for logging in error case.
451
            custom: Dictionary with the custom metadata.
452
        Return:
453
            Xml element as string.
454
        """
455
456
        _custom = Element('custom')
457
        for key, val in custom.items():
458
            xml_key = SubElement(_custom, key)
459
            try:
460
                xml_key.text = val
461
            except ValueError as e:
462
                logger.warning(
463
                    "Not possible to parse custom tag for VT %s: %s", vt_id, e
464
                )
465
        return tostring(_custom).decode('utf-8')
466
467
    @staticmethod
468
    def get_severities_vt_as_xml_str(vt_id: str, severities: Dict) -> str:
469
        """ Return an xml element with severities as string.
470
        Arguments:
471
            vt_id: VT OID. Only used for logging in error case.
472
            severities: Dictionary with the severities.
473
        Return:
474
            Xml element as string.
475
        """
476
        _severities = Element('severities')
477
        _severity = SubElement(_severities, 'severity')
478
        if 'severity_base_vector' in severities:
479
            try:
480
                _severity.text = severities.get('severity_base_vector')
481
            except ValueError as e:
482
                logger.warning(
483
                    "Not possible to parse severity tag for vt %s: %s", vt_id, e
484
                )
485
        if 'severity_origin' in severities:
486
            _severity.set('origin', severities.get('severity_origin'))
487
        if 'severity_type' in severities:
488
            _severity.set('type', severities.get('severity_type'))
489
490
        return tostring(_severities).decode('utf-8')
491
492
    @staticmethod
493
    def get_params_vt_as_xml_str(vt_id: str, vt_params: Dict) -> str:
494
        """ Return an xml element with params formatted as string.
495
        Arguments:
496
            vt_id: VT OID. Only used for logging in error case.
497
            vt_params: Dictionary with the VT parameters.
498
        Return:
499
            Xml element as string.
500
        """
501
        vt_params_xml = Element('params')
502
        for _pref_id, prefs in vt_params.items():
503
            vt_param = Element('param')
504
            vt_param.set('type', prefs['type'])
505
            vt_param.set('id', _pref_id)
506
            xml_name = SubElement(vt_param, 'name')
507
            try:
508
                xml_name.text = prefs['name']
509
            except ValueError as e:
510
                logger.warning(
511
                    "Not possible to parse parameter for VT %s: %s", vt_id, e
512
                )
513
            if prefs['default']:
514
                xml_def = SubElement(vt_param, 'default')
515
                try:
516
                    xml_def.text = prefs['default']
517
                except ValueError as e:
518
                    logger.warning(
519
                        "Not possible to parse default parameter for VT %s: %s",
520
                        vt_id,
521
                        e,
522
                    )
523
            vt_params_xml.append(vt_param)
524
525
        return tostring(vt_params_xml).decode('utf-8')
526
527
    @staticmethod
528
    def get_refs_vt_as_xml_str(vt_id: str, vt_refs: Dict) -> str:
529
        """ Return an xml element with references formatted as string.
530
        Arguments:
531
            vt_id: VT OID. Only used for logging in error case.
532
            vt_refs: Dictionary with the VT references.
533
        Return:
534
            Xml element as string.
535
        """
536
        vt_refs_xml = Element('refs')
537
        for ref_type, ref_values in vt_refs.items():
538
            for value in ref_values:
539
                vt_ref = Element('ref')
540
                if ref_type == "xref" and value:
541
                    for xref in value.split(', '):
542
                        try:
543
                            _type, _id = xref.split(':', 1)
544
                        except ValueError:
545
                            logger.error(
546
                                'Not possible to parse xref %s for VT %s',
547
                                xref,
548
                                vt_id,
549
                            )
550
                            continue
551
                        vt_ref.set('type', _type.lower())
552
                        vt_ref.set('id', _id)
553
                elif value:
554
                    vt_ref.set('type', ref_type.lower())
555
                    vt_ref.set('id', value)
556
                else:
557
                    continue
558
                vt_refs_xml.append(vt_ref)
559
560
        return tostring(vt_refs_xml).decode('utf-8')
561
562
    @staticmethod
563
    def get_dependencies_vt_as_xml_str(
564
        vt_id: str, vt_dependencies: List
565
    ) -> str:
566
        """ Return  an xml element with dependencies as string.
567
        Arguments:
568
            vt_id: VT OID. Only used for logging in error case.
569
            vt_dependencies: List with the VT dependencies.
570
        Return:
571
            Xml element as string.
572
        """
573
        vt_deps_xml = Element('dependencies')
574
        for dep in vt_dependencies:
575
            _vt_dep = Element('dependency')
576
            try:
577
                _vt_dep.set('vt_id', dep)
578
            except (ValueError, TypeError):
579
                logger.error(
580
                    'Not possible to add dependency %s for VT %s', dep, vt_id
581
                )
582
                continue
583
            vt_deps_xml.append(_vt_dep)
584
585
        return tostring(vt_deps_xml).decode('utf-8')
586
587
    @staticmethod
588
    def get_creation_time_vt_as_xml_str(
589
        vt_id: str, vt_creation_time: str
590
    ) -> str:
591
        """ Return creation time as string.
592
        Arguments:
593
            vt_id: VT OID. Only used for logging in error case.
594
            vt_creation_time: String with the VT creation time.
595
        Return:
596
           Xml element as string.
597
        """
598
        _time = Element('creation_time')
599
        try:
600
            _time.text = vt_creation_time
601
        except ValueError as e:
602
            logger.warning(
603
                "Not possible to parse creation time for VT %s: %s", vt_id, e
604
            )
605
        return tostring(_time).decode('utf-8')
606
607
    @staticmethod
608
    def get_modification_time_vt_as_xml_str(
609
        vt_id: str, vt_modification_time: str
610
    ) -> str:
611
        """ Return modification time as string.
612
        Arguments:
613
            vt_id: VT OID. Only used for logging in error case.
614
            vt_modification_time: String with the VT modification time.
615
        Return:
616
            Xml element as string.
617
        """
618
        _time = Element('modification_time')
619
        try:
620
            _time.text = vt_modification_time
621
        except ValueError as e:
622
            logger.warning(
623
                "Not possible to parse modification time for VT %s: %s",
624
                vt_id,
625
                e,
626
            )
627
        return tostring(_time).decode('utf-8')
628
629
    @staticmethod
630
    def get_summary_vt_as_xml_str(vt_id: str, summary: str) -> str:
631
        """ Return summary as string.
632
        Arguments:
633
            vt_id: VT OID. Only used for logging in error case.
634
            summary: String with a VT summary.
635
        Return:
636
            Xml element as string.
637
        """
638
        _summary = Element('summary')
639
        try:
640
            _summary.text = summary
641
        except ValueError as e:
642
            logger.warning(
643
                "Not possible to parse summary tag for VT %s: %s", vt_id, e
644
            )
645
        return tostring(_summary).decode('utf-8')
646
647
    @staticmethod
648
    def get_impact_vt_as_xml_str(vt_id: str, impact) -> str:
649
        """ Return impact as string.
650
651
        Arguments:
652
            vt_id (str): VT OID. Only used for logging in error case.
653
            impact (str): String which explain the vulneravility impact.
654
        Return:
655
            string: xml element as string.
656
        """
657
        _impact = Element('impact')
658
        try:
659
            _impact.text = impact
660
        except ValueError as e:
661
            logger.warning(
662
                "Not possible to parse impact tag for VT %s: %s", vt_id, e
663
            )
664
        return tostring(_impact).decode('utf-8')
665
666
    @staticmethod
667
    def get_affected_vt_as_xml_str(vt_id: str, affected: str) -> str:
668
        """ Return affected as string.
669
        Arguments:
670
            vt_id: VT OID. Only used for logging in error case.
671
            affected: String which explain what is affected.
672
        Return:
673
            Xml element as string.
674
        """
675
        _affected = Element('affected')
676
        try:
677
            _affected.text = affected
678
        except ValueError as e:
679
            logger.warning(
680
                "Not possible to parse affected tag for VT %s: %s", vt_id, e
681
            )
682
        return tostring(_affected).decode('utf-8')
683
684
    @staticmethod
685
    def get_insight_vt_as_xml_str(vt_id: str, insight: str) -> str:
686
        """ Return insight as string.
687
        Arguments:
688
            vt_id: VT OID. Only used for logging in error case.
689
            insight: String giving an insight of the vulnerability.
690
        Return:
691
            Xml element as string.
692
        """
693
        _insight = Element('insight')
694
        try:
695
            _insight.text = insight
696
        except ValueError as e:
697
            logger.warning(
698
                "Not possible to parse insight tag for VT %s: %s", vt_id, e
699
            )
700
        return tostring(_insight).decode('utf-8')
701
702
    @staticmethod
703
    def get_solution_vt_as_xml_str(
704
        vt_id: str,
705
        solution: str,
706
        solution_type: Optional[str] = None,
707
        solution_method: Optional[str] = None,
708
    ) -> str:
709
        """ Return solution as string.
710
        Arguments:
711
            vt_id: VT OID. Only used for logging in error case.
712
            solution: String giving a possible solution.
713
            solution_type: A solution type
714
            solution_method: A solution method
715
        Return:
716
            Xml element as string.
717
        """
718
        _solution = Element('solution')
719
        try:
720
            _solution.text = solution
721
        except ValueError as e:
722
            logger.warning(
723
                "Not possible to parse solution tag for VT %s: %s", vt_id, e
724
            )
725
        if solution_type:
726
            _solution.set('type', solution_type)
727
        if solution_method:
728
            _solution.set('method', solution_method)
729
        return tostring(_solution).decode('utf-8')
730
731
    @staticmethod
732
    def get_detection_vt_as_xml_str(
733
        vt_id: str,
734
        detection: Optional[str] = None,
735
        qod_type: Optional[str] = None,
736
        qod: Optional[str] = None,
737
    ) -> str:
738
        """ Return detection as string.
739
        Arguments:
740
            vt_id: VT OID. Only used for logging in error case.
741
            detection: String which explain how the vulnerability
742
              was detected.
743
            qod_type: qod type.
744
            qod: qod value.
745
        Return:
746
            Xml element as string.
747
        """
748
        _detection = Element('detection')
749
        if detection:
750
            try:
751
                _detection.text = detection
752
            except ValueError as e:
753
                logger.warning(
754
                    "Not possible to parse detection tag for VT %s: %s",
755
                    vt_id,
756
                    e,
757
                )
758
        if qod_type:
759
            _detection.set('qod_type', qod_type)
760
        elif qod:
761
            _detection.set('qod', qod)
762
763
        return tostring(_detection).decode('utf-8')
764
765
    @property
766
    def is_running_as_root(self) -> bool:
767
        """ Check if it is running as root user."""
768
        if self._is_running_as_root is not None:
769
            return self._is_running_as_root
770
771
        self._is_running_as_root = False
772
        if geteuid() == 0:
773
            self._is_running_as_root = True
774
775
        return self._is_running_as_root
776
777
    @property
778
    def sudo_available(self) -> bool:
779
        """ Checks that sudo is available """
780
        if self._sudo_available is not None:
781
            return self._sudo_available
782
783
        if self.is_running_as_root:
784
            self._sudo_available = False
785
            return self._sudo_available
786
787
        self._sudo_available = Openvas.check_sudo()
788
789
        return self._sudo_available
790
791
    def check(self) -> bool:
792
        """ Checks that openvas command line tool is found and
793
        is executable. """
794
        has_openvas = Openvas.check()
795
        if not has_openvas:
796
            logger.error(
797
                'openvas executable not available. Please install openvas'
798
                ' into your PATH.'
799
            )
800
        return has_openvas
801
802
    def update_progress(self, scan_id: str, current_host: str, msg: str):
803
        """ Calculate percentage and update the scan status of a host
804
        for the progress bar.
805
        Arguments:
806
            scan_id: Scan ID to identify the current scan process.
807
            current_host: Host in the target to be updated.
808
            msg: String with launched and total plugins.
809
        """
810
        try:
811
            launched, total = msg.split('/')
812
        except ValueError:
813
            return
814
        if float(total) == 0:
815
            return
816
        elif float(total) == -1:
817
            host_prog = 100
818
        else:
819
            host_prog = (float(launched) / float(total)) * 100
820
        self.set_scan_host_progress(
821
            scan_id, host=current_host, progress=host_prog
822
        )
823
824
    def report_openvas_scan_status(
825
        self, scan_db: ScanDB, scan_id: str, current_host: str
826
    ):
827
        """ Get all status entries from redis kb.
828
829
        Arguments:
830
            scan_id: Scan ID to identify the current scan.
831
            current_host: Host to be updated.
832
        """
833
        res = scan_db.get_scan_status()
834
        while res:
835
            self.update_progress(scan_id, current_host, res)
836
            res = scan_db.get_scan_status()
837
838
    def get_severity_score(self, vt_aux: dict) -> Optional[float]:
839
        """ Return the severity score for the given oid.
840
        Arguments:
841
            vt_aux: VT element from which to get the severity vector
842
        Returns:
843
            The calculated cvss base value. None if there is no severity
844
            vector or severity type is not cvss base version 2.
845
        """
846
        if vt_aux:
847
            severity_type = vt_aux['severities'].get('severity_type')
848
            severity_vector = vt_aux['severities'].get('severity_base_vector')
849
850
            if severity_type == "cvss_base_v2" and severity_vector:
851
                return CVSS.cvss_base_v2_value(severity_vector)
852
853
        return None
854
855
    def report_openvas_results(
856
        self, db: BaseDB, scan_id: str, current_host: str
857
    ):
858
        """ Get all result entries from redis kb. """
859
860
        vthelper = VtHelper(self.nvti)
861
862
        res = db.get_result()
863
        res_list = ResultList()
864
        host_progress_batch = dict()
865
        finished_host_batch = list()
866
        while res:
867
            msg = res.split('|||')
868
            roid = msg[3].strip()
869
            rqod = ''
870
            rname = ''
871
            rhostname = msg[1].strip() if msg[1] else ''
872
            host_is_dead = "Host dead" in msg[4]
873
            vt_aux = None
874
875
            if roid and not host_is_dead:
876
                vt_aux = vthelper.get_single_vt(roid)
877
878
            if not vt_aux and not host_is_dead:
879
                logger.warning('Invalid VT oid %s for a result', roid)
880
881
            if vt_aux:
882
                if vt_aux.get('qod_type'):
883
                    qod_t = vt_aux.get('qod_type')
884
                    rqod = self.nvti.QOD_TYPES[qod_t]
885
                elif vt_aux.get('qod'):
886
                    rqod = vt_aux.get('qod')
887
888
                rname = vt_aux.get('name')
889
890
            if msg[0] == 'ERRMSG':
891
                res_list.add_scan_error_to_list(
892
                    host=current_host,
893
                    hostname=rhostname,
894
                    name=rname,
895
                    value=msg[4],
896
                    port=msg[2],
897
                    test_id=roid,
898
                )
899
900
            if msg[0] == 'LOG':
901
                res_list.add_scan_log_to_list(
902
                    host=current_host,
903
                    hostname=rhostname,
904
                    name=rname,
905
                    value=msg[4],
906
                    port=msg[2],
907
                    qod=rqod,
908
                    test_id=roid,
909
                )
910
911
            if msg[0] == 'HOST_DETAIL':
912
                res_list.add_scan_host_detail_to_list(
913
                    host=current_host,
914
                    hostname=rhostname,
915
                    name=rname,
916
                    value=msg[4],
917
                )
918
919
            if msg[0] == 'ALARM':
920
                rseverity = self.get_severity_score(vt_aux)
921
                res_list.add_scan_alarm_to_list(
922
                    host=current_host,
923
                    hostname=rhostname,
924
                    name=rname,
925
                    value=msg[4],
926
                    port=msg[2],
927
                    test_id=roid,
928
                    severity=rseverity,
929
                    qod=rqod,
930
                )
931
932
            # To process non scanned dead hosts when
933
            # test_alive_host_only in openvas is enable
934
            if msg[0] == 'DEADHOST':
935
                hosts = msg[3].split(',')
936
                for _host in hosts:
937
                    if _host:
938
                        host_progress_batch[_host] = 100
939
                        finished_host_batch.append(_host)
940
                        res_list.add_scan_log_to_list(
941
                            host=_host,
942
                            hostname=rhostname,
943
                            name=rname,
944
                            value=msg[4],
945
                            port=msg[2],
946
                            qod=rqod,
947
                            test_id='',
948
                        )
949
                        timestamp = time.ctime(time.time())
950
                        res_list.add_scan_log_to_list(
951
                            host=_host, name='HOST_START', value=timestamp,
952
                        )
953
                        res_list.add_scan_log_to_list(
954
                            host=_host, name='HOST_END', value=timestamp,
955
                        )
956
957
            res = db.get_result()
958
959
        # Insert result batch into the scan collection table.
960
        if len(res_list):
961
            self.scan_collection.add_result_list(scan_id, res_list)
962
963
        if host_progress_batch:
964
            self.set_scan_progress_batch(
965
                scan_id, host_progress=host_progress_batch
966
            )
967
968
        if finished_host_batch:
969
            self.set_scan_host_finished(
970
                scan_id, finished_hosts=finished_host_batch
971
            )
972
973
    def report_openvas_timestamp_scan_host(
974
        self, scan_db: ScanDB, scan_id: str, host: str
975
    ):
976
        """ Get start and end timestamp of a host scan from redis kb. """
977
        timestamp = scan_db.get_host_scan_end_time()
978
        if timestamp:
979
            self.add_scan_log(
980
                scan_id, host=host, name='HOST_END', value=timestamp
981
            )
982
            return
983
984
        timestamp = scan_db.get_host_scan_start_time()
985
        if timestamp:
986
            self.add_scan_log(
987
                scan_id, host=host, name='HOST_START', value=timestamp
988
            )
989
            return
990
991
    def is_openvas_process_alive(
992
        self, kbdb: BaseDB, ovas_pid: str, openvas_scan_id: str
993
    ) -> bool:
994
        parent_exists = True
995
        parent = None
996
        try:
997
            parent = psutil.Process(int(ovas_pid))
998
        except psutil.NoSuchProcess:
999
            logger.debug('Process with pid %s already stopped', ovas_pid)
1000
            parent_exists = False
1001
        except TypeError:
1002
            logger.debug(
1003
                'Scan with ID %s never started and stopped unexpectedly',
1004
                openvas_scan_id,
1005
            )
1006
            parent_exists = False
1007
1008
        is_zombie = False
1009
        if parent and parent.status() == psutil.STATUS_ZOMBIE:
1010
            is_zombie = True
1011
1012
        if (not parent_exists or is_zombie) and kbdb:
1013
            if kbdb and kbdb.scan_is_stopped(openvas_scan_id):
1014
                return True
1015
            return False
1016
1017
        return True
1018
1019
    def stop_scan_cleanup(  # pylint: disable=arguments-differ
1020
        self, global_scan_id: str
1021
    ):
1022
        """ Set a key in redis to indicate the wrapper is stopped.
1023
        It is done through redis because it is a new multiprocess
1024
        instance and it is not possible to reach the variables
1025
        of the grandchild process. Send SIGUSR2 to openvas to stop
1026
        each running scan."""
1027
1028
        openvas_scan_id, kbdb = self.main_db.find_kb_database_by_scan_id(
1029
            global_scan_id
1030
        )
1031
        if kbdb:
1032
            kbdb.stop_scan(openvas_scan_id)
1033
            ovas_pid = kbdb.get_scan_process_id()
1034
1035
            parent = None
1036
            try:
1037
                parent = psutil.Process(int(ovas_pid))
1038
            except psutil.NoSuchProcess:
1039
                logger.debug('Process with pid %s already stopped', ovas_pid)
1040
            except TypeError:
1041
                logger.debug(
1042
                    'Scan with ID %s never started and stopped unexpectedly',
1043
                    openvas_scan_id,
1044
                )
1045
1046
            if parent:
1047
                can_stop_scan = Openvas.stop_scan(
1048
                    openvas_scan_id,
1049
                    not self.is_running_as_root and self.sudo_available,
1050
                )
1051
                if not can_stop_scan:
1052
                    logger.debug(
1053
                        'Not possible to stop scan process: %s.', parent,
1054
                    )
1055
                    return False
1056
1057
                logger.debug('Stopping process: %s', parent)
1058
1059
                while parent:
1060
                    try:
1061
                        parent = psutil.Process(int(ovas_pid))
1062
                    except psutil.NoSuchProcess:
1063
                        parent = None
1064
1065
            for scan_db in kbdb.get_scan_databases():
1066
                self.main_db.release_database(scan_db)
1067
1068
    def exec_scan(self, scan_id: str):
1069
        """ Starts the OpenVAS scanner for scan_id scan. """
1070
        do_not_launch = False
1071
        kbdb = self.main_db.get_new_kb_database()
1072
        scan_prefs = PreferenceHandler(
1073
            scan_id, kbdb, self.scan_collection, self.nvti
1074
        )
1075
        openvas_scan_id = scan_prefs.prepare_openvas_scan_id_for_openvas()
1076
        scan_prefs.prepare_target_for_openvas()
1077
1078
        if not scan_prefs.prepare_ports_for_openvas():
1079
            self.add_scan_error(
1080
                scan_id, name='', host='', value='No port list defined.'
1081
            )
1082
            do_not_launch = True
1083
1084
        # Set credentials
1085
        if not scan_prefs.prepare_credentials_for_openvas():
1086
            self.add_scan_error(
1087
                scan_id, name='', host='', value='Malformed credential.'
1088
            )
1089
            do_not_launch = True
1090
1091
        if not scan_prefs.prepare_plugins_for_openvas():
1092
            self.add_scan_error(
1093
                scan_id, name='', host='', value='No VTS to run.'
1094
            )
1095
            do_not_launch = True
1096
1097
        scan_prefs.prepare_main_kbindex_for_openvas()
1098
        scan_prefs.prepare_host_options_for_openvas()
1099
        scan_prefs.prepare_scan_params_for_openvas(OSPD_PARAMS)
1100
        scan_prefs.prepare_reverse_lookup_opt_for_openvas()
1101
        scan_prefs.prepare_alive_test_option_for_openvas()
1102
1103
        # Release memory used for scan preferences.
1104
        del scan_prefs
1105
1106
        if do_not_launch:
1107
            self.main_db.release_database(kbdb)
1108
            return
1109
1110
        result = Openvas.start_scan(
1111
            openvas_scan_id,
1112
            not self.is_running_as_root and self.sudo_available,
1113
            self._niceness,
1114
        )
1115
1116
        if result is None:
1117
            self.main_db.release_database(kbdb)
1118
            return
1119
1120
        ovas_pid = result.pid
1121
        kbdb.add_scan_process_id(ovas_pid)
1122
        logger.debug('pid = %s', ovas_pid)
1123
1124
        # Wait until the scanner starts and loads all the preferences.
1125
        while kbdb.get_status(openvas_scan_id) == 'new':
1126
            res = result.poll()
1127
            if res and res < 0:
1128
                self.stop_scan_cleanup(scan_id)
1129
                logger.error(
1130
                    'It was not possible run the task %s, since openvas ended '
1131
                    'unexpectedly with errors during launching.',
1132
                    scan_id,
1133
                )
1134
                return
1135
1136
            time.sleep(1)
1137
1138
        no_id_found = False
1139
        while True:
1140
            if not kbdb.target_is_finished(
1141
                scan_id
1142
            ) and not self.is_openvas_process_alive(
1143
                kbdb, ovas_pid, openvas_scan_id
1144
            ):
1145
                logger.error(
1146
                    'Task %s was unexpectedly stopped or killed.', scan_id,
1147
                )
1148
                self.add_scan_error(
1149
                    scan_id,
1150
                    name='',
1151
                    host='',
1152
                    value='Task was unexpectedly stopped or killed.',
1153
                )
1154
                kbdb.stop_scan(openvas_scan_id)
1155
                for scan_db in kbdb.get_scan_databases():
1156
                    self.main_db.release_database(scan_db)
1157
                self.main_db.release_database(kbdb)
1158
                return
1159
1160
            time.sleep(3)
1161
            # Check if the client stopped the whole scan
1162
            if kbdb.scan_is_stopped(openvas_scan_id):
1163
                # clean main_db
1164
                self.main_db.release_database(kbdb)
1165
                return
1166
1167
            self.report_openvas_results(kbdb, scan_id, "")
1168
1169
            for scan_db in kbdb.get_scan_databases():
1170
1171
                id_aux = scan_db.get_scan_id()
1172
                if not id_aux:
1173
                    continue
1174
1175
                if id_aux == openvas_scan_id:
1176
                    no_id_found = False
1177
                    current_host = scan_db.get_host_ip()
1178
1179
                    self.report_openvas_results(scan_db, scan_id, current_host)
1180
                    self.report_openvas_scan_status(
1181
                        scan_db, scan_id, current_host
1182
                    )
1183
                    self.report_openvas_timestamp_scan_host(
1184
                        scan_db, scan_id, current_host
1185
                    )
1186
1187
                    if scan_db.host_is_finished(openvas_scan_id):
1188
                        self.set_scan_host_finished(
1189
                            scan_id, finished_hosts=current_host
1190
                        )
1191
                        self.report_openvas_scan_status(
1192
                            scan_db, scan_id, current_host
1193
                        )
1194
                        self.report_openvas_timestamp_scan_host(
1195
                            scan_db, scan_id, current_host
1196
                        )
1197
1198
                        kbdb.remove_scan_database(scan_db)
1199
                        self.main_db.release_database(scan_db)
1200
1201
            # Scan end. No kb in use for this scan id
1202
            if no_id_found and kbdb.target_is_finished(scan_id):
1203
                break
1204
1205
            no_id_found = True
1206
1207
        # Delete keys from KB related to this scan task.
1208
        self.main_db.release_database(kbdb)
1209
1210
1211
def main():
1212
    """ OSP openvas main function. """
1213
    daemon_main('OSPD - openvas', OSPDopenvas)
1214
1215
1216
if __name__ == '__main__':
1217
    main()
1218