Passed
Pull Request — master (#170)
by Juan José
01:39
created

ospd.ospd.OSPDaemon.process_credentials_elements()   A

Complexity

Conditions 4

Size

Total Lines 41
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nop 1
dl 0
loc 41
rs 9.8
c 0
b 0
f 0
1
# Copyright (C) 2014-2018 Greenbone Networks GmbH
2
#
3
# SPDX-License-Identifier: GPL-2.0-or-later
4
#
5
# This program is free software; you can redistribute it and/or
6
# modify it under the terms of the GNU General Public License
7
# as published by the Free Software Foundation; either version 2
8
# of the License, or (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18
19
# pylint: disable=too-many-lines
20
21
""" OSP Daemon core class.
22
"""
23
24
25
# This is needed for older pythons as our current module is called the same
26
# as the package we are in ...
27
# Another solution would be to rename that file.
28
from __future__ import absolute_import
29
30
import logging
31
import socket
32
import ssl
33
import multiprocessing
34
import re
35
import time
36
import os
37
import subprocess
38
39
from xml.etree.ElementTree import Element, SubElement
40
41
import defusedxml.ElementTree as secET
42
43
from ospd import __version__
44
from ospd.errors import OspdCommandError, OspdError
45
from ospd.misc import ScanCollection, ResultType, ScanStatus, valid_uuid
46
from ospd.network import resolve_hostname, target_str_to_list
47
from ospd.server import BaseServer
48
from ospd.vtfilter import VtsFilter
49
from ospd.xml import simple_response_str, get_result_xml
50
51
logger = logging.getLogger(__name__)
52
53
PROTOCOL_VERSION = "1.2"
54
55
SCHEDULER_CHECK_PERIOD = 5  # in seconds
56
57
GVMCG_TITLES = [
58
    'cpu-*',
59
    'proc',
60
    'mem',
61
    'swap',
62
    'load',
63
    'df-*',
64
    'disk-sd[a-z][0-9]-rw',
65
    'disk-sd[a-z][0-9]-load',
66
    'disk-sd[a-z][0-9]-io-load',
67
    'interface-eth*-traffic',
68
    'interface-eth*-err-rate',
69
    'interface-eth*-err',
70
    'sensors-*_temperature-*',
71
    'sensors-*_fanspeed-*',
72
    'sensors-*_voltage-*',
73
    'titles',
74
]
75
76
BASE_SCANNER_PARAMS = {
77
    'debug_mode': {
78
        'type': 'boolean',
79
        'name': 'Debug Mode',
80
        'default': 0,
81
        'mandatory': 0,
82
        'description': 'Whether to get extra scan debug information.',
83
    },
84
    'dry_run': {
85
        'type': 'boolean',
86
        'name': 'Dry Run',
87
        'default': 0,
88
        'mandatory': 0,
89
        'description': 'Whether to dry run scan.',
90
    },
91
}
92
93
COMMANDS_TABLE = {
94
    'start_scan': {
95
        'description': 'Start a new scan.',
96
        'attributes': {
97
            'target': 'Target host to scan',
98
            'ports': 'Ports list to scan',
99
            'scan_id': 'Optional UUID value to use as scan ID',
100
            'parallel': 'Optional nummer of parallel target to scan',
101
        },
102
        'elements': None,
103
    },
104
    'stop_scan': {
105
        'description': 'Stop a currently running scan.',
106
        'attributes': {'scan_id': 'ID of scan to stop.'},
107
        'elements': None,
108
    },
109
    'help': {
110
        'description': 'Print the commands help.',
111
        'attributes': {'format': 'Help format. Could be text or xml.'},
112
        'elements': None,
113
    },
114
    'get_scans': {
115
        'description': 'List the scans in buffer.',
116
        'attributes': {
117
            'scan_id': 'ID of a specific scan to get.',
118
            'details': 'Whether to return the full scan report.',
119
        },
120
        'elements': None,
121
    },
122
    'get_vts': {
123
        'description': 'List of available vulnerability tests.',
124
        'attributes': {
125
            'vt_id': 'ID of a specific vulnerability test to get.',
126
            'filter': 'Optional filter to get an specific vt collection.',
127
        },
128
        'elements': None,
129
    },
130
    'delete_scan': {
131
        'description': 'Delete a finished scan.',
132
        'attributes': {'scan_id': 'ID of scan to delete.'},
133
        'elements': None,
134
    },
135
    'get_version': {
136
        'description': 'Return various versions.',
137
        'attributes': None,
138
        'elements': None,
139
    },
140
    'get_scanner_details': {
141
        'description': 'Return scanner description and parameters',
142
        'attributes': None,
143
        'elements': None,
144
    },
145
    'get_performance': {
146
        'description': 'Return system report',
147
        'attributes': {
148
            'start': 'Time of first data point in report.',
149
            'end': 'Time of last data point in report.',
150
            'title': 'Name of report.',
151
        },
152
        'elements': None,
153
    },
154
}
155
156
157
class OSPDaemon:
158
159
    """ Daemon class for OSP traffic handling.
160
161
    Every scanner wrapper should subclass it and make necessary additions and
162
    changes.
163
164
    * Add any needed parameters in __init__.
165
    * Implement check() method which verifies scanner availability and other
166
      environment related conditions.
167
    * Implement process_scan_params and exec_scan methods which are
168
      specific to handling the <start_scan> command, executing the wrapped
169
      scanner and storing the results.
170
    * exec_scan() should return 0 if host is dead or not reached, 1 if host is
171
      alive and 2 if scan error or status is unknown.
172
    * Implement other methods that assert to False such as get_scanner_name,
173
      get_scanner_version.
174
    * Use Call set_command_attributes at init time to add scanner command
175
      specific options eg. the w3af profile for w3af wrapper.
176
    """
177
178
    def __init__(
179
        self, *, customvtfilter=None, **kwargs
180
    ):  # pylint: disable=unused-argument
181
        """ Initializes the daemon's internal data. """
182
        self.scan_collection = ScanCollection()
183
        self.scan_processes = dict()
184
185
        self.daemon_info = dict()
186
        self.daemon_info['name'] = "OSPd"
187
        self.daemon_info['version'] = __version__
188
        self.daemon_info['description'] = "No description"
189
190
        self.scanner_info = dict()
191
        self.scanner_info['name'] = 'No name'
192
        self.scanner_info['version'] = 'No version'
193
        self.scanner_info['description'] = 'No description'
194
195
        self.server_version = None  # Set by the subclass.
196
197
        self.scaninfo_store_time = kwargs.get('scaninfo_store_time')
198
199
        self.protocol_version = PROTOCOL_VERSION
200
201
        self.commands = COMMANDS_TABLE
202
203
        self.scanner_params = dict()
204
205
        for name, param in BASE_SCANNER_PARAMS.items():
206
            self.add_scanner_param(name, param)
207
208
        self.vts = dict()
209
        self.vt_id_pattern = re.compile("[0-9a-zA-Z_\\-:.]{1,80}")
210
        self.vts_version = None
211
212
        if customvtfilter:
213
            self.vts_filter = customvtfilter
214
        else:
215
            self.vts_filter = VtsFilter()
216
217
    def init(self):
218
        """ Should be overridden by a subclass if the initialization is costly.
219
220
            Will be called before check.
221
        """
222
223
    def set_command_attributes(self, name, attributes):
224
        """ Sets the xml attributes of a specified command. """
225
        if self.command_exists(name):
226
            command = self.commands.get(name)
227
            command['attributes'] = attributes
228
229
    def add_scanner_param(self, name, scanner_param):
230
        """ Add a scanner parameter. """
231
232
        assert name
233
        assert scanner_param
234
        self.scanner_params[name] = scanner_param
235
        command = self.commands.get('start_scan')
236
        command['elements'] = {
237
            'scanner_params': {
238
                k: v['name'] for k, v in self.scanner_params.items()
239
            }
240
        }
241
242
    def add_vt(
243
        self,
244
        vt_id,
245
        name=None,
246
        vt_params=None,
247
        vt_refs=None,
248
        custom=None,
249
        vt_creation_time=None,
250
        vt_modification_time=None,
251
        vt_dependencies=None,
252
        summary=None,
253
        impact=None,
254
        affected=None,
255
        insight=None,
256
        solution=None,
257
        solution_t=None,
258
        detection=None,
259
        qod_t=None,
260
        qod_v=None,
261
        severities=None,
262
    ):
263
        """ Add a vulnerability test information.
264
        """
265
266
        if not vt_id:
267
            raise OspdError('Invalid vt_id {}'.format(vt_id))
268
269
        if self.vt_id_pattern.fullmatch(vt_id) is None:
270
            raise OspdError('Invalid vt_id {}'.format(vt_id))
271
272
        if vt_id in self.vts:
273
            raise OspdError('vt_id {} already exists'.format(vt_id))
274
275
        if name is None:
276
            name = ''
277
278
        vt = {'name': name}
279
        if custom is not None:
280
            vt["custom"] = custom
281
        if vt_params is not None:
282
            vt["vt_params"] = vt_params
283
        if vt_refs is not None:
284
            vt["vt_refs"] = vt_refs
285
        if vt_dependencies is not None:
286
            vt["vt_dependencies"] = vt_dependencies
287
        if vt_creation_time is not None:
288
            vt["creation_time"] = vt_creation_time
289
        if vt_modification_time is not None:
290
            vt["modification_time"] = vt_modification_time
291
        if summary is not None:
292
            vt["summary"] = summary
293
        if impact is not None:
294
            vt["impact"] = impact
295
        if affected is not None:
296
            vt["affected"] = affected
297
        if insight is not None:
298
            vt["insight"] = insight
299
300
        if solution is not None:
301
            vt["solution"] = solution
302
            if solution_t is not None:
303
                vt["solution_type"] = solution_t
304
305
        if detection is not None:
306
            vt["detection"] = detection
307
308
        if qod_t is not None:
309
            vt["qod_type"] = qod_t
310
        elif qod_v is not None:
311
            vt["qod"] = qod_v
312
313
        if severities is not None:
314
            vt["severities"] = severities
315
316
        self.vts[vt_id] = vt
317
318
    def set_vts_version(self, vts_version):
319
        """ Add into the vts dictionary an entry to identify the
320
        vts version.
321
322
        Parameters:
323
            vts_version (str): Identifies a unique vts version.
324
        """
325
        if not vts_version:
326
            raise OspdCommandError(
327
                'A vts_version parameter is required', 'set_vts_version'
328
            )
329
        self.vts_version = vts_version
330
331
    def get_vts_version(self):
332
        """Return the vts version.
333
        """
334
        return self.vts_version
335
336
    def command_exists(self, name):
337
        """ Checks if a commands exists. """
338
        return name in self.commands.keys()
339
340
    def get_scanner_name(self):
341
        """ Gives the wrapped scanner's name. """
342
        return self.scanner_info['name']
343
344
    def get_scanner_version(self):
345
        """ Gives the wrapped scanner's version. """
346
        return self.scanner_info['version']
347
348
    def get_scanner_description(self):
349
        """ Gives the wrapped scanner's description. """
350
        return self.scanner_info['description']
351
352
    def get_server_version(self):
353
        """ Gives the specific OSP server's version. """
354
        assert self.server_version
355
        return self.server_version
356
357
    def get_protocol_version(self):
358
        """ Gives the OSP's version. """
359
        return self.protocol_version
360
361
    def _preprocess_scan_params(self, xml_params):
362
        """ Processes the scan parameters. """
363
        params = {}
364
        for param in xml_params:
365
            params[param.tag] = param.text or ''
366
        # Set default values.
367
        for key in self.scanner_params:
368
            if key not in params:
369
                params[key] = self.get_scanner_param_default(key)
370
                if self.get_scanner_param_type(key) == 'selection':
371
                    params[key] = params[key].split('|')[0]
372
        # Validate values.
373
        for key in params:
374
            param_type = self.get_scanner_param_type(key)
375
            if not param_type:
376
                continue
377
            if param_type in ['integer', 'boolean']:
378
                try:
379
                    params[key] = int(params[key])
380
                except ValueError:
381
                    raise OspdCommandError(
382
                        'Invalid %s value' % key, 'start_scan'
383
                    )
384
            if param_type == 'boolean':
385
                if params[key] not in [0, 1]:
386
                    raise OspdCommandError(
387
                        'Invalid %s value' % key, 'start_scan'
388
                    )
389
            elif param_type == 'selection':
390
                selection = self.get_scanner_param_default(key).split('|')
391
                if params[key] not in selection:
392
                    raise OspdCommandError(
393
                        'Invalid %s value' % key, 'start_scan'
394
                    )
395
            if self.get_scanner_param_mandatory(key) and params[key] == '':
396
                raise OspdCommandError(
397
                    'Mandatory %s value is missing' % key, 'start_scan'
398
                )
399
        return params
400
401
    def process_scan_params(self, params):
402
        """ This method is to be overridden by the child classes if necessary
403
        """
404
        return params
405
406
    def process_vts_params(self, scanner_vts):
407
        """ Receive an XML object with the Vulnerability Tests an their
408
        parameters to be use in a scan and return a dictionary.
409
410
        @param: XML element with vt subelements. Each vt has an
411
                id attribute. Optional parameters can be included
412
                as vt child.
413
                Example form:
414
                <vt_selection>
415
                  <vt_single id='vt1' />
416
                  <vt_single id='vt2'>
417
                    <vt_value id='param1'>value</vt_value>
418
                  </vt_single>
419
                  <vt_group filter='family=debian'/>
420
                  <vt_group filter='family=general'/>
421
                </vt_selection>
422
423
        @return: Dictionary containing the vts attribute and subelements,
424
                 like the VT's id and VT's parameters.
425
                 Example form:
426
                 {'vt1': {},
427
                  'vt2': {'value_id': 'value'},
428
                  'vt_groups': ['family=debian', 'family=general']}
429
        """
430
        vt_selection = {}
431
        filters = list()
432
        for vt in scanner_vts:
433
            if vt.tag == 'vt_single':
434
                vt_id = vt.attrib.get('id')
435
                vt_selection[vt_id] = {}
436
                for vt_value in vt:
437
                    if not vt_value.attrib.get('id'):
438
                        raise OspdCommandError(
439
                            'Invalid VT preference. No attribute id',
440
                            'start_scan',
441
                        )
442
                    vt_value_id = vt_value.attrib.get('id')
443
                    vt_value_value = vt_value.text if vt_value.text else ''
444
                    vt_selection[vt_id][vt_value_id] = vt_value_value
445
            if vt.tag == 'vt_group':
446
                vts_filter = vt.attrib.get('filter', None)
447
                if vts_filter is None:
448
                    raise OspdCommandError(
449
                        'Invalid VT group. No filter given.', 'start_scan'
450
                    )
451
                filters.append(vts_filter)
452
        vt_selection['vt_groups'] = filters
453
        return vt_selection
454
455
    @staticmethod
456
    def process_credentials_elements(cred_tree):
457
        """ Receive an XML object with the credentials to run
458
        a scan against a given target.
459
460
        @param:
461
        <credentials>
462
          <credential type="up" service="ssh" port="22">
463
            <username>scanuser</username>
464
            <password>mypass</password>
465
          </credential>
466
          <credential type="up" service="smb">
467
            <username>smbuser</username>
468
            <password>mypass</password>
469
          </credential>
470
        </credentials>
471
472
        @return: Dictionary containing the credentials for a given target.
473
                 Example form:
474
                 {'ssh': {'type': type,
475
                          'port': port,
476
                          'username': username,
477
                          'password': pass,
478
                        },
479
                  'smb': {'type': type,
480
                          'username': username,
481
                          'password': pass,
482
                         },
483
                   }
484
        """
485
        credentials = {}
486
        for credential in cred_tree:
487
            service = credential.attrib.get('service')
488
            credentials[service] = {}
489
            credentials[service]['type'] = credential.attrib.get('type')
490
            if service == 'ssh':
491
                credentials[service]['port'] = credential.attrib.get('port')
492
            for param in credential:
493
                credentials[service][param.tag] = param.text
494
495
        return credentials
496
497
    @classmethod
498
    def process_targets_element(cls, scanner_target):
499
        """ Receive an XML object with the target, ports and credentials to run
500
        a scan against.
501
502
        @param: XML element with target subelements. Each target has <hosts>
503
        and <ports> subelements. Hosts can be a single host, a host range,
504
        a comma-separated host list or a network address.
505
        <ports> and  <credentials> are optional. Therefore each ospd-scanner
506
        should check for a valid ones if needed.
507
508
                Example form:
509
                <targets>
510
                  <target>
511
                    <hosts>localhosts</hosts>
512
                    <exclude_hosts>localhost1</exclude_hosts>
513
                    <ports>80,443</ports>
514
                  </target>
515
                  <target>
516
                    <hosts>192.168.0.0/24</hosts>
517
                    <ports>22</ports>
518
                    <credentials>
519
                      <credential type="up" service="ssh" port="22">
520
                        <username>scanuser</username>
521
                        <password>mypass</password>
522
                      </credential>
523
                      <credential type="up" service="smb">
524
                        <username>smbuser</username>
525
                        <password>mypass</password>
526
                      </credential>
527
                    </credentials>
528
                  </target>
529
                </targets>
530
531
        @return: A list of [hosts, port, {credentials}, exclude_hosts] list.
532
                 Example form:
533
                 [['localhosts', '80,43', '', 'localhosts1'],
534
                  ['192.168.0.0/24', '22', {'smb': {'type': type,
535
                                                    'port': port,
536
                                                    'username': username,
537
                                                    'password': pass,
538
                                                   }}], '']
539
        """
540
541
        target_list = []
542
        for target in scanner_target:
543
            exclude_hosts = ''
544
            ports = ''
545
            credentials = {}
546
            for child in target:
547
                if child.tag == 'hosts':
548
                    hosts = child.text
549
                if child.tag == 'exclude_hosts':
550
                    exclude_hosts = child.text
551
                if child.tag == 'ports':
552
                    ports = child.text
553
                if child.tag == 'credentials':
554
                    credentials = cls.process_credentials_elements(child)
555
            if hosts:
0 ignored issues
show
introduced by
The variable hosts does not seem to be defined for all execution paths.
Loading history...
556
                target_list.append([hosts, ports, credentials, exclude_hosts])
557
            else:
558
                raise OspdCommandError('No target to scan', 'start_scan')
559
560
        return target_list
561
562
    def handle_start_scan_command(self, scan_et):
563
        """ Handles <start_scan> command.
564
565
        @return: Response string for <start_scan> command.
566
        """
567
568
        target_str = scan_et.attrib.get('target')
569
        ports_str = scan_et.attrib.get('ports')
570
        # For backward compatibility, if target and ports attributes are set,
571
        # <targets> element is ignored.
572
        if target_str is None or ports_str is None:
573
            target_list = scan_et.find('targets')
574
            if target_list is None or len(target_list) == 0:
575
                raise OspdCommandError('No targets or ports', 'start_scan')
576
            else:
577
                scan_targets = self.process_targets_element(target_list)
578
        else:
579
            scan_targets = []
580
            for single_target in target_str_to_list(target_str):
581
                scan_targets.append([single_target, ports_str, '', ''])
582
583
        scan_id = scan_et.attrib.get('scan_id')
584
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
585
            raise OspdCommandError('Invalid scan_id UUID', 'start_scan')
586
587
        try:
588
            parallel = int(scan_et.attrib.get('parallel', '1'))
589
            if parallel < 1 or parallel > 20:
590
                parallel = 1
591
        except ValueError:
592
            raise OspdCommandError(
593
                'Invalid value for parallel scans. ' 'It must be a number',
594
                'start_scan',
595
            )
596
597
        scanner_params = scan_et.find('scanner_params')
598
        if scanner_params is None:
599
            raise OspdCommandError('No scanner_params element', 'start_scan')
600
601
        params = self._preprocess_scan_params(scanner_params)
602
603
        # VTS is an optional element. If present should not be empty.
604
        vt_selection = {}
605
        scanner_vts = scan_et.find('vt_selection')
606
        if scanner_vts is not None:
607
            if len(scanner_vts) == 0:
608
                raise OspdCommandError('VTs list is empty', 'start_scan')
609
            else:
610
                vt_selection = self.process_vts_params(scanner_vts)
611
612
        # Dry run case.
613
        if 'dry_run' in params and int(params['dry_run']):
614
            scan_func = self.dry_run_scan
615
            scan_params = None
616
        else:
617
            scan_func = self.start_scan
618
            scan_params = self.process_scan_params(params)
619
620
        scan_id = self.create_scan(
621
            scan_id, scan_targets, scan_params, vt_selection
622
        )
623
        scan_process = multiprocessing.Process(
624
            target=scan_func, args=(scan_id, scan_targets, parallel)
625
        )
626
        self.scan_processes[scan_id] = scan_process
627
        scan_process.start()
628
        id_ = Element('id')
629
        id_.text = scan_id
630
        return simple_response_str('start_scan', 200, 'OK', id_)
631
632
    def handle_stop_scan_command(self, scan_et):
633
        """ Handles <stop_scan> command.
634
635
        @return: Response string for <stop_scan> command.
636
        """
637
638
        scan_id = scan_et.attrib.get('scan_id')
639
        if scan_id is None or scan_id == '':
640
            raise OspdCommandError('No scan_id attribute', 'stop_scan')
641
        self.stop_scan(scan_id)
642
643
        return simple_response_str('stop_scan', 200, 'OK')
644
645
    def stop_scan(self, scan_id):
646
        scan_process = self.scan_processes.get(scan_id)
647
        if not scan_process:
648
            raise OspdCommandError(
649
                'Scan not found {0}.'.format(scan_id), 'stop_scan'
650
            )
651
        if not scan_process.is_alive():
652
            raise OspdCommandError(
653
                'Scan already stopped or finished.', 'stop_scan'
654
            )
655
656
        self.set_scan_status(scan_id, ScanStatus.STOPPED)
657
        logger.info('%s: Scan stopping %s.', scan_id, scan_process.ident)
658
        self.stop_scan_cleanup(scan_id)
659
        try:
660
            scan_process.terminate()
661
        except AttributeError:
662
            logger.debug('%s: The scanner task stopped unexpectedly.', scan_id)
663
664
        try:
665
            os.killpg(os.getpgid(scan_process.ident), 15)
666
        except ProcessLookupError as e:
667
            logger.info(
668
                '%s: Scan already stopped %s.', scan_id, scan_process.ident
669
            )
670
671
        if scan_process.ident != os.getpid():
672
            scan_process.join()
673
        logger.info('%s: Scan stopped.', scan_id)
674
675
    @staticmethod
676
    def stop_scan_cleanup(scan_id):
677
        """ Should be implemented by subclass in case of a clean up before
678
        terminating is needed. """
679
680
    @staticmethod
681
    def target_is_finished(scan_id):
682
        """ Should be implemented by subclass in case of a check before
683
        stopping is needed. """
684
685
    def exec_scan(self, scan_id, target):
686
        """ Asserts to False. Should be implemented by subclass. """
687
        raise NotImplementedError
688
689
    def finish_scan(self, scan_id):
690
        """ Sets a scan as finished. """
691
        self.set_scan_progress(scan_id, 100)
692
        self.set_scan_status(scan_id, ScanStatus.FINISHED)
693
        logger.info("%s: Scan finished.", scan_id)
694
695
    def get_daemon_name(self):
696
        """ Gives osp daemon's name. """
697
        return self.daemon_info['name']
698
699
    def get_daemon_version(self):
700
        """ Gives osp daemon's version. """
701
        return self.daemon_info['version']
702
703
    def get_scanner_param_type(self, param):
704
        """ Returns type of a scanner parameter. """
705
        assert isinstance(param, str)
706
        entry = self.scanner_params.get(param)
707
        if not entry:
708
            return None
709
        return entry.get('type')
710
711
    def get_scanner_param_mandatory(self, param):
712
        """ Returns if a scanner parameter is mandatory. """
713
        assert isinstance(param, str)
714
        entry = self.scanner_params.get(param)
715
        if not entry:
716
            return False
717
        return entry.get('mandatory')
718
719
    def get_scanner_param_default(self, param):
720
        """ Returns default value of a scanner parameter. """
721
        assert isinstance(param, str)
722
        entry = self.scanner_params.get(param)
723
        if not entry:
724
            return None
725
        return entry.get('default')
726
727
    def get_scanner_params_xml(self):
728
        """ Returns the OSP Daemon's scanner params in xml format. """
729
        scanner_params = Element('scanner_params')
730
        for param_id, param in self.scanner_params.items():
731
            param_xml = SubElement(scanner_params, 'scanner_param')
732
            for name, value in [('id', param_id), ('type', param['type'])]:
733
                param_xml.set(name, value)
734
            for name, value in [
735
                ('name', param['name']),
736
                ('description', param['description']),
737
                ('default', param['default']),
738
                ('mandatory', param['mandatory']),
739
            ]:
740
                elem = SubElement(param_xml, name)
741
                elem.text = str(value)
742
        return scanner_params
743
744
    def handle_client_stream(self, stream):
745
        """ Handles stream of data received from client. """
746
747
        data = b''
748
749
        while True:
750
            try:
751
                buf = stream.read()
752
                if not buf:
753
                    break
754
755
                data += buf
756
            except (AttributeError, ValueError) as message:
757
                logger.error(message)
758
                return
759
            except (ssl.SSLError) as exception:
760
                logger.debug('Error: %s', exception)
761
                break
762
            except (socket.timeout) as exception:
763
                break
764
765
        if len(data) <= 0:
766
            logger.debug("Empty client stream")
767
            return
768
769
        try:
770
            response = self.handle_command(data)
771
        except OspdCommandError as exception:
772
            response = exception.as_xml()
773
            logger.debug('Command error: %s', exception.message)
774
        except Exception:  # pylint: disable=broad-except
775
            logger.exception('While handling client command:')
776
            exception = OspdCommandError('Fatal error', 'error')
777
            response = exception.as_xml()
778
779
        stream.write(response)
780
        stream.close()
781
782
    def parallel_scan(self, scan_id, target):
783
        """ Starts the scan with scan_id. """
784
        try:
785
            ret = self.exec_scan(scan_id, target)
786
            if ret == 0:
787
                logger.info("%s: Host scan dead.", target)
788
            elif ret == 1:
789
                logger.info("%s: Host scan alived.", target)
790
            elif ret == 2:
791
                logger.info("%s: Scan error or status unknown.", target)
792
            else:
793
                logger.debug('%s: No host status returned', target)
794
        except Exception as e:  # pylint: disable=broad-except
795
            self.add_scan_error(
796
                scan_id,
797
                name='',
798
                host=target,
799
                value='Host process failure (%s).' % e,
800
            )
801
            logger.exception('While scanning %s:', target)
802
        else:
803
            logger.info("%s: Host scan finished.", target)
804
805
    def check_pending_target(self, scan_id, multiscan_proc):
806
        """ Check if a scan process is still alive. In case the process
807
        finished or is stopped, removes the process from the multiscan
808
        _process list.
809
        Processes dead and with progress < 100% are considered stopped
810
        or with failures. Then will try to stop the other runnings (target)
811
        scans owned by the same task.
812
813
        @input scan_id        Scan_id of the whole scan.
814
        @input multiscan_proc A list with the scan process which
815
                              may still be alive.
816
817
        @return Actualized list with current running scan processes.
818
        """
819
        for running_target_proc, running_target_id in multiscan_proc:
820
            if not running_target_proc.is_alive():
821
                target_prog = self.get_scan_target_progress(
822
                    scan_id, running_target_id
823
                )
824
825
                _not_finished_clean = target_prog < 100
826
                _not_stopped = (
827
                    self.get_scan_status(scan_id) != ScanStatus.STOPPED
828
                )
829
830
                if _not_finished_clean and _not_stopped:
831
                    if not self.target_is_finished(scan_id):
832
                        self.stop_scan(scan_id)
833
834
                running_target = (running_target_proc, running_target_id)
835
                multiscan_proc.remove(running_target)
836
837
        return multiscan_proc
838
839
    def calculate_progress(self, scan_id):
840
        """ Calculate the total scan progress from the
841
        partial target progress. """
842
843
        t_prog = dict()
844
        for target in self.get_scan_target(scan_id):
845
            t_prog[target] = self.get_scan_target_progress(scan_id, target)
846
        return sum(t_prog.values()) / len(t_prog)
847
848
    def process_exclude_hosts(self, scan_id, target_list):
849
        """ Process the exclude hosts before launching the scans.
850
        Set exclude hosts as finished with 100% to calculate
851
        the scan progress."""
852
853
        for target, _, _, exclude_hosts in target_list:
854
            exc_hosts_list = ''
855
            if not exclude_hosts:
856
                continue
857
            exc_hosts_list = target_str_to_list(exclude_hosts)
858
            for host in exc_hosts_list:
859
                self.set_scan_host_finished(scan_id, target, host)
860
                self.set_scan_host_progress(scan_id, target, host, 100)
861
862
    def start_scan(self, scan_id, targets, parallel=1):
863
        """ Handle N parallel scans if 'parallel' is greater than 1. """
864
865
        os.setsid()
866
        multiscan_proc = []
867
        logger.info("%s: Scan started.", scan_id)
868
        target_list = targets
869
        if target_list is None or not target_list:
870
            raise OspdCommandError('Erroneous targets list', 'start_scan')
871
872
        self.process_exclude_hosts(scan_id, target_list)
873
874
        for _index, target in enumerate(target_list):
875
            while len(multiscan_proc) >= parallel:
876
                progress = self.calculate_progress(scan_id)
877
                self.set_scan_progress(scan_id, progress)
878
                multiscan_proc = self.check_pending_target(
879
                    scan_id, multiscan_proc
880
                )
881
                time.sleep(1)
882
883
            # If the scan status is stopped, does not launch anymore target
884
            # scans
885
            if self.get_scan_status(scan_id) == ScanStatus.STOPPED:
886
                return
887
888
            logger.debug(
889
                "%s: Host scan started on ports %s.", target[0], target[1]
890
            )
891
            scan_process = multiprocessing.Process(
892
                target=self.parallel_scan, args=(scan_id, target[0])
893
            )
894
            multiscan_proc.append((scan_process, target[0]))
895
            scan_process.start()
896
            self.set_scan_status(scan_id, ScanStatus.RUNNING)
897
898
        # Wait until all single target were scanned
899
        while multiscan_proc:
900
            multiscan_proc = self.check_pending_target(scan_id, multiscan_proc)
901
            if multiscan_proc:
902
                progress = self.calculate_progress(scan_id)
903
                self.set_scan_progress(scan_id, progress)
904
            time.sleep(1)
905
906
        # Only set the scan as finished if the scan was not stopped.
907
        if self.get_scan_status(scan_id) != ScanStatus.STOPPED:
908
            self.finish_scan(scan_id)
909
910
    def dry_run_scan(self, scan_id, targets, parallel):
911
        """ Dry runs a scan. """
912
913
        os.setsid()
914
        for _, target in enumerate(targets):
915
            host = resolve_hostname(target[0])
916
            if host is None:
917
                logger.info("Couldn't resolve %s.", target[0])
918
                continue
919
            port = self.get_scan_ports(scan_id, target=target[0])
920
            logger.info("%s:%s: Dry run mode.", host, port)
921
            self.add_scan_log(
922
                scan_id, name='', host=host, value='Dry run result'
923
            )
924
        self.finish_scan(scan_id)
925
926
    def handle_timeout(self, scan_id, host):
927
        """ Handles scanner reaching timeout error. """
928
        self.add_scan_error(
929
            scan_id,
930
            host=host,
931
            name="Timeout",
932
            value="{0} exec timeout.".format(self.get_scanner_name()),
933
        )
934
935
    def set_scan_host_finished(self, scan_id, target, host):
936
        """ Add the host in a list of finished hosts """
937
        self.scan_collection.set_host_finished(scan_id, target, host)
938
939
    def set_scan_progress(self, scan_id, progress):
940
        """ Sets scan_id scan's progress which is a number
941
        between 0 and 100. """
942
        self.scan_collection.set_progress(scan_id, progress)
943
944
    def set_scan_host_progress(self, scan_id, target, host, progress):
945
        """ Sets host's progress which is part of target. """
946
        self.scan_collection.set_host_progress(scan_id, target, host, progress)
947
948
    def set_scan_status(self, scan_id, status):
949
        """ Set the scan's status."""
950
        self.scan_collection.set_status(scan_id, status)
951
952
    def get_scan_status(self, scan_id):
953
        """ Get scan_id scans's status."""
954
        return self.scan_collection.get_status(scan_id)
955
956
    def scan_exists(self, scan_id):
957
        """ Checks if a scan with ID scan_id is in collection.
958
959
        @return: 1 if scan exists, 0 otherwise.
960
        """
961
        return self.scan_collection.id_exists(scan_id)
962
963
    def handle_get_scans_command(self, scan_et):
964
        """ Handles <get_scans> command.
965
966
        @return: Response string for <get_scans> command.
967
        """
968
969
        scan_id = scan_et.attrib.get('scan_id')
970
        details = scan_et.attrib.get('details')
971
        pop_res = scan_et.attrib.get('pop_results')
972
        if details and details == '0':
973
            details = False
974
        else:
975
            details = True
976
            if pop_res and pop_res == '1':
977
                pop_res = True
978
            else:
979
                pop_res = False
980
981
        responses = []
982
        if scan_id and scan_id in self.scan_collection.ids_iterator():
983
            self.check_scan_process(scan_id)
984
            scan = self.get_scan_xml(scan_id, details, pop_res)
985
            responses.append(scan)
986
        elif scan_id:
987
            text = "Failed to find scan '{0}'".format(scan_id)
988
            return simple_response_str('get_scans', 404, text)
989
        else:
990
            for scan_id in self.scan_collection.ids_iterator():
991
                self.check_scan_process(scan_id)
992
                scan = self.get_scan_xml(scan_id, details, pop_res)
993
                responses.append(scan)
994
        return simple_response_str('get_scans', 200, 'OK', responses)
995
996
    def handle_get_vts_command(self, vt_et):
997
        """ Handles <get_vts> command.
998
        The <get_vts> element accept two optional arguments.
999
        vt_id argument receives a single vt id.
1000
        filter argument receives a filter selecting a sub set of vts.
1001
        If both arguments are given, the vts which match with the filter
1002
        are return.
1003
1004
        @return: Response string for <get_vts> command.
1005
        """
1006
1007
        vt_id = vt_et.attrib.get('vt_id')
1008
        vt_filter = vt_et.attrib.get('filter')
1009
1010
        if vt_id and vt_id not in self.vts:
1011
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
1012
            return simple_response_str('get_vts', 404, text)
1013
1014
        filtered_vts = None
1015
        if vt_filter:
1016
            filtered_vts = self.vts_filter.get_filtered_vts_list(
1017
                self.vts, vt_filter
1018
            )
1019
1020
        responses = []
1021
1022
        vts_xml = self.get_vts_xml(vt_id, filtered_vts)
1023
1024
        responses.append(vts_xml)
1025
1026
        return simple_response_str('get_vts', 200, 'OK', responses)
1027
1028
    def handle_get_performance(self, scan_et):
1029
        """ Handles <get_performance> command.
1030
1031
        @return: Response string for <get_performance> command.
1032
        """
1033
        start = scan_et.attrib.get('start')
1034
        end = scan_et.attrib.get('end')
1035
        titles = scan_et.attrib.get('titles')
1036
1037
        cmd = ['gvmcg']
1038
        if start:
1039
            try:
1040
                int(start)
1041
            except ValueError:
1042
                raise OspdCommandError(
1043
                    'Start argument must be integer.', 'get_performance'
1044
                )
1045
            cmd.append(start)
1046
1047
        if end:
1048
            try:
1049
                int(end)
1050
            except ValueError:
1051
                raise OspdCommandError(
1052
                    'End argument must be integer.', 'get_performance'
1053
                )
1054
            cmd.append(end)
1055
1056
        if titles:
1057
            combined = "(" + ")|(".join(GVMCG_TITLES) + ")"
1058
            forbidden = "^[^|&;]+$"
1059
            if re.match(combined, titles) and re.match(forbidden, titles):
1060
                cmd.append(titles)
1061
            else:
1062
                raise OspdCommandError(
1063
                    'Arguments not allowed', 'get_performance'
1064
                )
1065
1066
        try:
1067
            output = subprocess.check_output(cmd)
1068
        except (
1069
            subprocess.CalledProcessError,
1070
            PermissionError,
1071
            FileNotFoundError,
1072
        ) as e:
1073
            raise OspdCommandError(
1074
                'Bogus get_performance format. %s' % e, 'get_performance'
1075
            )
1076
1077
        return simple_response_str(
1078
            'get_performance', 200, 'OK', output.decode()
1079
        )
1080
1081
    def handle_help_command(self, scan_et):
1082
        """ Handles <help> command.
1083
1084
        @return: Response string for <help> command.
1085
        """
1086
        help_format = scan_et.attrib.get('format')
1087
        if help_format is None or help_format == "text":
1088
            # Default help format is text.
1089
            return simple_response_str('help', 200, 'OK', self.get_help_text())
1090
        elif help_format == "xml":
1091
            text = self.get_xml_str(self.commands)
1092
            return simple_response_str('help', 200, 'OK', text)
1093
        raise OspdCommandError('Bogus help format', 'help')
1094
1095
    def get_help_text(self):
1096
        """ Returns the help output in plain text format."""
1097
1098
        txt = str('\n')
1099
        for name, info in self.commands.items():
1100
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
1101
            if info['attributes']:
1102
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
1103
                for attrname, attrdesc in info['attributes'].items():
1104
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
1105
                    command_txt = ''.join([command_txt, attr_txt])
1106
            if info['elements']:
1107
                command_txt = ''.join(
1108
                    [
1109
                        command_txt,
1110
                        "\t Elements:\n",
1111
                        self.elements_as_text(info['elements']),
1112
                    ]
1113
                )
1114
            txt = ''.join([txt, command_txt])
1115
        return txt
1116
1117
    def elements_as_text(self, elems, indent=2):
1118
        """ Returns the elems dictionary as formatted plain text. """
1119
        assert elems
1120
        text = ""
1121
        for elename, eledesc in elems.items():
1122
            if isinstance(eledesc, dict):
1123
                desc_txt = self.elements_as_text(eledesc, indent + 2)
1124
                desc_txt = ''.join(['\n', desc_txt])
1125
            elif isinstance(eledesc, str):
1126
                desc_txt = ''.join([eledesc, '\n'])
1127
            else:
1128
                assert False, "Only string or dictionary"
1129
            ele_txt = "\t{0}{1: <22} {2}".format(
1130
                ' ' * indent, elename, desc_txt
0 ignored issues
show
introduced by
The variable desc_txt does not seem to be defined for all execution paths.
Loading history...
1131
            )
1132
            text = ''.join([text, ele_txt])
1133
        return text
1134
1135
    def handle_delete_scan_command(self, scan_et):
1136
        """ Handles <delete_scan> command.
1137
1138
        @return: Response string for <delete_scan> command.
1139
        """
1140
        scan_id = scan_et.attrib.get('scan_id')
1141
        if scan_id is None:
1142
            return simple_response_str(
1143
                'delete_scan', 404, 'No scan_id attribute'
1144
            )
1145
1146
        if not self.scan_exists(scan_id):
1147
            text = "Failed to find scan '{0}'".format(scan_id)
1148
            return simple_response_str('delete_scan', 404, text)
1149
        self.check_scan_process(scan_id)
1150
        if self.delete_scan(scan_id):
1151
            return simple_response_str('delete_scan', 200, 'OK')
1152
        raise OspdCommandError('Scan in progress', 'delete_scan')
1153
1154
    def delete_scan(self, scan_id):
1155
        """ Deletes scan_id scan from collection.
1156
1157
        @return: 1 if scan deleted, 0 otherwise.
1158
        """
1159
        if self.get_scan_status(scan_id) == ScanStatus.RUNNING:
1160
            return 0
1161
1162
        try:
1163
            del self.scan_processes[scan_id]
1164
        except KeyError:
1165
            logger.debug('Scan process for %s not found', scan_id)
1166
        return self.scan_collection.delete_scan(scan_id)
1167
1168
    def get_scan_results_xml(self, scan_id, pop_res):
1169
        """ Gets scan_id scan's results in XML format.
1170
1171
        @return: String of scan results in xml.
1172
        """
1173
        results = Element('results')
1174
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1175
            results.append(get_result_xml(result))
1176
1177
        logger.debug('Returning %d results', len(results))
1178
        return results
1179
1180
    def get_xml_str(self, data):
1181
        """ Creates a string in XML Format using the provided data structure.
1182
1183
        @param: Dictionary of xml tags and their elements.
1184
1185
        @return: String of data in xml format.
1186
        """
1187
1188
        responses = []
1189
        for tag, value in data.items():
1190
            elem = Element(tag)
1191
            if isinstance(value, dict):
1192
                for val in self.get_xml_str(value):
1193
                    elem.append(val)
1194
            elif isinstance(value, list):
1195
                elem.text = ', '.join(value)
1196
            else:
1197
                elem.text = value
1198
            responses.append(elem)
1199
        return responses
1200
1201
    def get_scan_xml(self, scan_id, detailed=True, pop_res=False):
1202
        """ Gets scan in XML format.
1203
1204
        @return: String of scan in XML format.
1205
        """
1206
        if not scan_id:
1207
            return Element('scan')
1208
1209
        target = ','.join(self.get_scan_target(scan_id))
1210
        progress = self.get_scan_progress(scan_id)
1211
        status = self.get_scan_status(scan_id)
1212
        start_time = self.get_scan_start_time(scan_id)
1213
        end_time = self.get_scan_end_time(scan_id)
1214
        response = Element('scan')
1215
        for name, value in [
1216
            ('id', scan_id),
1217
            ('target', target),
1218
            ('progress', progress),
1219
            ('status', status.name.lower()),
1220
            ('start_time', start_time),
1221
            ('end_time', end_time),
1222
        ]:
1223
            response.set(name, str(value))
1224
        if detailed:
1225
            response.append(self.get_scan_results_xml(scan_id, pop_res))
1226
        return response
1227
1228
    @staticmethod
1229
    def get_custom_vt_as_xml_str(
1230
        vt_id, custom
1231
    ):  # pylint: disable=unused-argument
1232
        """ Create a string representation of the XML object from the
1233
        custom data object.
1234
        This needs to be implemented by each ospd wrapper, in case
1235
        custom elements for VTs are used.
1236
1237
        The custom XML object which is returned will be embedded
1238
        into a <custom></custom> element.
1239
1240
        @return: XML object as string for custom data.
1241
        """
1242
        return ''
1243
1244
    @staticmethod
1245
    def get_params_vt_as_xml_str(
1246
        vt_id, vt_params
1247
    ):  # pylint: disable=unused-argument
1248
        """ Create a string representation of the XML object from the
1249
        vt_params data object.
1250
        This needs to be implemented by each ospd wrapper, in case
1251
        vt_params elements for VTs are used.
1252
1253
        The params XML object which is returned will be embedded
1254
        into a <params></params> element.
1255
1256
        @return: XML object as string for vt parameters data.
1257
        """
1258
        return ''
1259
1260
    @staticmethod
1261
    def get_refs_vt_as_xml_str(
1262
        vt_id, vt_refs
1263
    ):  # pylint: disable=unused-argument
1264
        """ Create a string representation of the XML object from the
1265
        refs data object.
1266
        This needs to be implemented by each ospd wrapper, in case
1267
        refs elements for VTs are used.
1268
1269
        The refs XML object which is returned will be embedded
1270
        into a <refs></refs> element.
1271
1272
        @return: XML object as string for vt references data.
1273
        """
1274
        return ''
1275
1276
    @staticmethod
1277
    def get_dependencies_vt_as_xml_str(
1278
        vt_id, vt_dependencies
1279
    ):  # pylint: disable=unused-argument
1280
        """ Create a string representation of the XML object from the
1281
        vt_dependencies data object.
1282
        This needs to be implemented by each ospd wrapper, in case
1283
        vt_dependencies elements for VTs are used.
1284
1285
        The vt_dependencies XML object which is returned will be embedded
1286
        into a <dependencies></dependencies> element.
1287
1288
        @return: XML object as string for vt dependencies data.
1289
        """
1290
        return ''
1291
1292
    @staticmethod
1293
    def get_creation_time_vt_as_xml_str(
1294
        vt_id, vt_creation_time
1295
    ):  # pylint: disable=unused-argument
1296
        """ Create a string representation of the XML object from the
1297
        vt_creation_time data object.
1298
        This needs to be implemented by each ospd wrapper, in case
1299
        vt_creation_time elements for VTs are used.
1300
1301
        The vt_creation_time XML object which is returned will be embedded
1302
        into a <vt_creation_time></vt_creation_time> element.
1303
1304
        @return: XML object as string for vt creation time data.
1305
        """
1306
        return ''
1307
1308
    @staticmethod
1309
    def get_modification_time_vt_as_xml_str(
1310
        vt_id, vt_modification_time
1311
    ):  # pylint: disable=unused-argument
1312
        """ Create a string representation of the XML object from the
1313
        vt_modification_time data object.
1314
        This needs to be implemented by each ospd wrapper, in case
1315
        vt_modification_time elements for VTs are used.
1316
1317
        The vt_modification_time XML object which is returned will be embedded
1318
        into a <vt_modification_time></vt_modification_time> element.
1319
1320
        @return: XML object as string for vt references data.
1321
        """
1322
        return ''
1323
1324
    @staticmethod
1325
    def get_summary_vt_as_xml_str(
1326
        vt_id, summary
1327
    ):  # pylint: disable=unused-argument
1328
        """ Create a string representation of the XML object from the
1329
        summary data object.
1330
        This needs to be implemented by each ospd wrapper, in case
1331
        summary elements for VTs are used.
1332
1333
        The summary XML object which is returned will be embedded
1334
        into a <summary></summary> element.
1335
1336
        @return: XML object as string for summary data.
1337
        """
1338
        return ''
1339
1340
    @staticmethod
1341
    def get_impact_vt_as_xml_str(
1342
        vt_id, impact
1343
    ):  # pylint: disable=unused-argument
1344
        """ Create a string representation of the XML object from the
1345
        impact data object.
1346
        This needs to be implemented by each ospd wrapper, in case
1347
        impact elements for VTs are used.
1348
1349
        The impact XML object which is returned will be embedded
1350
        into a <impact></impact> element.
1351
1352
        @return: XML object as string for impact data.
1353
        """
1354
        return ''
1355
1356
    @staticmethod
1357
    def get_affected_vt_as_xml_str(
1358
        vt_id, affected
1359
    ):  # pylint: disable=unused-argument
1360
        """ Create a string representation of the XML object from the
1361
        affected data object.
1362
        This needs to be implemented by each ospd wrapper, in case
1363
        affected elements for VTs are used.
1364
1365
        The affected XML object which is returned will be embedded
1366
        into a <affected></affected> element.
1367
1368
        @return: XML object as string for affected data.
1369
        """
1370
        return ''
1371
1372
    @staticmethod
1373
    def get_insight_vt_as_xml_str(
1374
        vt_id, insight
1375
    ):  # pylint: disable=unused-argument
1376
        """ Create a string representation of the XML object from the
1377
        insight data object.
1378
        This needs to be implemented by each ospd wrapper, in case
1379
        insight elements for VTs are used.
1380
1381
        The insight XML object which is returned will be embedded
1382
        into a <insight></insight> element.
1383
1384
        @return: XML object as string for insight data.
1385
        """
1386
        return ''
1387
1388
    @staticmethod
1389
    def get_solution_vt_as_xml_str(
1390
        vt_id, solution, solution_type=None
1391
    ):  # pylint: disable=unused-argument
1392
        """ Create a string representation of the XML object from the
1393
        solution data object.
1394
        This needs to be implemented by each ospd wrapper, in case
1395
        solution elements for VTs are used.
1396
1397
        The solution XML object which is returned will be embedded
1398
        into a <solution></solution> element.
1399
1400
        @return: XML object as string for solution data.
1401
        """
1402
        return ''
1403
1404
    @staticmethod
1405
    def get_detection_vt_as_xml_str(
1406
        vt_id, detection=None, qod_type=None, qod=None
1407
    ):  # pylint: disable=unused-argument
1408
        """ Create a string representation of the XML object from the
1409
        detection data object.
1410
        This needs to be implemented by each ospd wrapper, in case
1411
        detection elements for VTs are used.
1412
1413
        The detection XML object which is returned is an element with
1414
        tag <detection></detection> element
1415
1416
        @return: XML object as string for detection data.
1417
        """
1418
        return ''
1419
1420
    @staticmethod
1421
    def get_severities_vt_as_xml_str(
1422
        vt_id, severities
1423
    ):  # pylint: disable=unused-argument
1424
        """ Create a string representation of the XML object from the
1425
        severities data object.
1426
        This needs to be implemented by each ospd wrapper, in case
1427
        severities elements for VTs are used.
1428
1429
        The severities XML objects which are returned will be embedded
1430
        into a <severities></severities> element.
1431
1432
        @return: XML object as string for severities data.
1433
        """
1434
        return ''
1435
1436
    def get_vt_xml(self, vt_id):
1437
        """ Gets a single vulnerability test information in XML format.
1438
1439
        @return: String of single vulnerability test information in XML format.
1440
        """
1441
        if not vt_id:
1442
            return Element('vt')
1443
1444
        vt = self.vts.get(vt_id)
1445
1446
        name = vt.get('name')
1447
        vt_xml = Element('vt')
1448
        vt_xml.set('id', vt_id)
1449
1450
        for name, value in [('name', name)]:
1451
            elem = SubElement(vt_xml, name)
1452
            elem.text = str(value)
1453
1454
        if vt.get('vt_params'):
1455
            params_xml_str = self.get_params_vt_as_xml_str(
1456
                vt_id, vt.get('vt_params')
1457
            )
1458
            vt_xml.append(secET.fromstring(params_xml_str))
1459
1460
        if vt.get('vt_refs'):
1461
            refs_xml_str = self.get_refs_vt_as_xml_str(vt_id, vt.get('vt_refs'))
1462
            vt_xml.append(secET.fromstring(refs_xml_str))
1463
1464
        if vt.get('vt_dependencies'):
1465
            dependencies = self.get_dependencies_vt_as_xml_str(
1466
                vt_id, vt.get('vt_dependencies')
1467
            )
1468
            vt_xml.append(secET.fromstring(dependencies))
1469
1470
        if vt.get('creation_time'):
1471
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1472
                vt_id, vt.get('creation_time')
1473
            )
1474
            vt_xml.append(secET.fromstring(vt_ctime))
1475
1476
        if vt.get('modification_time'):
1477
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1478
                vt_id, vt.get('modification_time')
1479
            )
1480
            vt_xml.append(secET.fromstring(vt_mtime))
1481
1482
        if vt.get('summary'):
1483
            summary_xml_str = self.get_summary_vt_as_xml_str(
1484
                vt_id, vt.get('summary')
1485
            )
1486
            vt_xml.append(secET.fromstring(summary_xml_str))
1487
1488
        if vt.get('impact'):
1489
            impact_xml_str = self.get_impact_vt_as_xml_str(
1490
                vt_id, vt.get('impact')
1491
            )
1492
            vt_xml.append(secET.fromstring(impact_xml_str))
1493
1494
        if vt.get('affected'):
1495
            affected_xml_str = self.get_affected_vt_as_xml_str(
1496
                vt_id, vt.get('affected')
1497
            )
1498
            vt_xml.append(secET.fromstring(affected_xml_str))
1499
1500
        if vt.get('insight'):
1501
            insight_xml_str = self.get_insight_vt_as_xml_str(
1502
                vt_id, vt.get('insight')
1503
            )
1504
            vt_xml.append(secET.fromstring(insight_xml_str))
1505
1506
        if vt.get('solution'):
1507
            solution_xml_str = self.get_solution_vt_as_xml_str(
1508
                vt_id, vt.get('solution'), vt.get('solution_type')
1509
            )
1510
            vt_xml.append(secET.fromstring(solution_xml_str))
1511
1512
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1513
            detection_xml_str = self.get_detection_vt_as_xml_str(
1514
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod')
1515
            )
1516
            vt_xml.append(secET.fromstring(detection_xml_str))
1517
1518
        if vt.get('severities'):
1519
            severities_xml_str = self.get_severities_vt_as_xml_str(
1520
                vt_id, vt.get('severities')
1521
            )
1522
            vt_xml.append(secET.fromstring(severities_xml_str))
1523
1524
        if vt.get('custom'):
1525
            custom_xml_str = self.get_custom_vt_as_xml_str(
1526
                vt_id, vt.get('custom')
1527
            )
1528
            vt_xml.append(secET.fromstring(custom_xml_str))
1529
1530
        return vt_xml
1531
1532
    def get_vts_xml(self, vt_id=None, filtered_vts=None):
1533
        """ Gets collection of vulnerability test information in XML format.
1534
        If vt_id is specified, the collection will contain only this vt, if
1535
        found.
1536
        If no vt_id is specified or filtered_vts is None (default), the
1537
        collection will contain all vts. Otherwise those vts passed
1538
        in filtered_vts or vt_id are returned. In case of both vt_id and
1539
        filtered_vts are given, filtered_vts has priority.
1540
1541
        Arguments:
1542
            vt_id (vt_id, optional): ID of the vt to get.
1543
            filtered_vts (dict, optional): Filtered VTs collection.
1544
1545
        Return:
1546
            String of collection of vulnerability test information in
1547
            XML format.
1548
        """
1549
1550
        vts_xml = Element('vts')
1551
1552
        if filtered_vts is not None and len(filtered_vts) == 0:
1553
            return vts_xml
1554
1555
        if filtered_vts:
1556
            for vt_id in filtered_vts:
1557
                vts_xml.append(self.get_vt_xml(vt_id))
1558
        elif vt_id:
1559
            vts_xml.append(self.get_vt_xml(vt_id))
1560
        else:
1561
            for vt_id in self.vts:
1562
                vts_xml.append(self.get_vt_xml(vt_id))
1563
1564
        return vts_xml
1565
1566
    def handle_get_scanner_details(self):
1567
        """ Handles <get_scanner_details> command.
1568
1569
        @return: Response string for <get_scanner_details> command.
1570
        """
1571
        desc_xml = Element('description')
1572
        desc_xml.text = self.get_scanner_description()
1573
        details = [desc_xml, self.get_scanner_params_xml()]
1574
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1575
1576
    def handle_get_version_command(self):
1577
        """ Handles <get_version> command.
1578
1579
        @return: Response string for <get_version> command.
1580
        """
1581
        protocol = Element('protocol')
1582
        for name, value in [
1583
            ('name', 'OSP'),
1584
            ('version', self.get_protocol_version()),
1585
        ]:
1586
            elem = SubElement(protocol, name)
1587
            elem.text = value
1588
1589
        daemon = Element('daemon')
1590
        for name, value in [
1591
            ('name', self.get_daemon_name()),
1592
            ('version', self.get_daemon_version()),
1593
        ]:
1594
            elem = SubElement(daemon, name)
1595
            elem.text = value
1596
1597
        scanner = Element('scanner')
1598
        for name, value in [
1599
            ('name', self.get_scanner_name()),
1600
            ('version', self.get_scanner_version()),
1601
        ]:
1602
            elem = SubElement(scanner, name)
1603
            elem.text = value
1604
1605
        content = [protocol, daemon, scanner]
1606
1607
        if self.get_vts_version():
1608
            vts = Element('vts')
1609
            elem = SubElement(vts, 'version')
1610
            elem.text = self.get_vts_version()
1611
            content.append(vts)
1612
1613
        return simple_response_str('get_version', 200, 'OK', content)
1614
1615
    def handle_command(self, command):
1616
        """ Handles an osp command in a string.
1617
1618
        @return: OSP Response to command.
1619
        """
1620
        try:
1621
            tree = secET.fromstring(command)
1622
        except secET.ParseError:
1623
            logger.debug("Erroneous client input: %s", command)
1624
            raise OspdCommandError('Invalid data')
1625
1626
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1627
            raise OspdCommandError('Bogus command name')
1628
1629
        if tree.tag == "get_version":
1630
            return self.handle_get_version_command()
1631
        elif tree.tag == "start_scan":
1632
            return self.handle_start_scan_command(tree)
1633
        elif tree.tag == "stop_scan":
1634
            return self.handle_stop_scan_command(tree)
1635
        elif tree.tag == "get_scans":
1636
            return self.handle_get_scans_command(tree)
1637
        elif tree.tag == "get_vts":
1638
            return self.handle_get_vts_command(tree)
1639
        elif tree.tag == "delete_scan":
1640
            return self.handle_delete_scan_command(tree)
1641
        elif tree.tag == "help":
1642
            return self.handle_help_command(tree)
1643
        elif tree.tag == "get_scanner_details":
1644
            return self.handle_get_scanner_details()
1645
        elif tree.tag == "get_performance":
1646
            return self.handle_get_performance(tree)
1647
        else:
1648
            assert False, "Unhandled command: {0}".format(tree.tag)
1649
1650
    def check(self):
1651
        """ Asserts to False. Should be implemented by subclass. """
1652
        raise NotImplementedError
1653
1654
    def run(self, server: BaseServer):
1655
        """ Starts the Daemon, handling commands until interrupted.
1656
        """
1657
1658
        server.start(self.handle_client_stream)
1659
1660
        try:
1661
            while True:
1662
                time.sleep(10)
1663
                self.scheduler()
1664
                self.clean_forgotten_scans()
1665
        except KeyboardInterrupt:
1666
            logger.info("Received Ctrl-C shutting-down ...")
1667
        finally:
1668
            logger.info("Shutting-down server ...")
1669
            server.close()
1670
1671
    def scheduler(self):
1672
        """ Should be implemented by subclass in case of need
1673
        to run tasks periodically. """
1674
1675
    def create_scan(self, scan_id, targets, options, vts):
1676
        """ Creates a new scan.
1677
1678
        @target: Target to scan.
1679
        @options: Miscellaneous scan options.
1680
1681
        @return: New scan's ID.
1682
        """
1683
        if self.scan_exists(scan_id):
1684
            logger.info("Scan %s exists. Resuming scan.", scan_id)
1685
1686
        return self.scan_collection.create_scan(scan_id, targets, options, vts)
1687
1688
    def get_scan_options(self, scan_id):
1689
        """ Gives a scan's list of options. """
1690
        return self.scan_collection.get_options(scan_id)
1691
1692
    def set_scan_option(self, scan_id, name, value):
1693
        """ Sets a scan's option to a provided value. """
1694
        return self.scan_collection.set_option(scan_id, name, value)
1695
1696
    def clean_forgotten_scans(self):
1697
        """ Check for old stopped or finished scans which have not been
1698
        deleted and delete them if the are older than the set value."""
1699
1700
        if not self.scaninfo_store_time:
1701
            return
1702
1703
        for scan_id in list(self.scan_collection.ids_iterator()):
1704
            end_time = int(self.get_scan_end_time(scan_id))
1705
            scan_status = self.get_scan_status(scan_id)
1706
1707
            if ((scan_status == ScanStatus.STOPPED or
1708
                scan_status == ScanStatus.FINISHED) and
1709
                end_time
1710
            ):
1711
                stored_time = int(time.time()) - end_time
1712
                if stored_time > self.scaninfo_store_time * 3600:
1713
                    logger.debug(
1714
                        'Scan %s is older than %d hours and seems have been '
1715
                        'forgotten. Scan info will be deleted from the '
1716
                        'scan table',
1717
                        scan_id,
1718
                        self.scaninfo_store_time,
1719
                    )
1720
                    self.delete_scan(scan_id)
1721
1722
1723
    def check_scan_process(self, scan_id):
1724
        """ Check the scan's process, and terminate the scan if not alive. """
1725
        scan_process = self.scan_processes[scan_id]
1726
        progress = self.get_scan_progress(scan_id)
1727
        if progress < 100 and not scan_process.is_alive():
1728
            if not (self.get_scan_status(scan_id) == ScanStatus.STOPPED):
1729
                self.set_scan_status(scan_id, ScanStatus.STOPPED)
1730
                self.add_scan_error(
1731
                    scan_id, name="", host="", value="Scan process failure."
1732
                )
1733
                logger.info("%s: Scan stopped with errors.", scan_id)
1734
        elif progress == 100:
1735
            scan_process.join()
1736
1737
    def get_scan_progress(self, scan_id):
1738
        """ Gives a scan's current progress value. """
1739
        return self.scan_collection.get_progress(scan_id)
1740
1741
    def get_scan_target_progress(self, scan_id, target):
1742
        """ Gives a list with scan's current progress value of each target. """
1743
        return self.scan_collection.get_target_progress(scan_id, target)
1744
1745
    def get_scan_target(self, scan_id):
1746
        """ Gives a scan's target. """
1747
        return self.scan_collection.get_target_list(scan_id)
1748
1749
    def get_scan_ports(self, scan_id, target=''):
1750
        """ Gives a scan's ports list. """
1751
        return self.scan_collection.get_ports(scan_id, target)
1752
1753
    def get_scan_exclude_hosts(self, scan_id, target=''):
1754
        """ Gives a scan's exclude host list. If a target is passed gives
1755
        the exclude host list for the given target. """
1756
        return self.scan_collection.get_exclude_hosts(scan_id, target)
1757
1758
    def get_scan_credentials(self, scan_id, target=''):
1759
        """ Gives a scan's credential list. If a target is passed gives
1760
        the credential list for the given target. """
1761
        return self.scan_collection.get_credentials(scan_id, target)
1762
1763
    def get_scan_vts(self, scan_id):
1764
        """ Gives a scan's vts list. """
1765
        return self.scan_collection.get_vts(scan_id)
1766
1767
    def get_scan_unfinished_hosts(self, scan_id):
1768
        """ Get a list of unfinished hosts."""
1769
        return self.scan_collection.get_hosts_unfinished(scan_id)
1770
1771
    def get_scan_finished_hosts(self, scan_id):
1772
        """ Get a list of unfinished hosts."""
1773
        return self.scan_collection.get_hosts_finished(scan_id)
1774
1775
    def get_scan_start_time(self, scan_id):
1776
        """ Gives a scan's start time. """
1777
        return self.scan_collection.get_start_time(scan_id)
1778
1779
    def get_scan_end_time(self, scan_id):
1780
        """ Gives a scan's end time. """
1781
        return self.scan_collection.get_end_time(scan_id)
1782
1783
    def add_scan_log(
1784
        self,
1785
        scan_id,
1786
        host='',
1787
        hostname='',
1788
        name='',
1789
        value='',
1790
        port='',
1791
        test_id='',
1792
        qod='',
1793
    ):
1794
        """ Adds a log result to scan_id scan. """
1795
        self.scan_collection.add_result(
1796
            scan_id,
1797
            ResultType.LOG,
1798
            host,
1799
            hostname,
1800
            name,
1801
            value,
1802
            port,
1803
            test_id,
1804
            0.0,
1805
            qod,
1806
        )
1807
1808
    def add_scan_error(
1809
        self, scan_id, host='', hostname='', name='', value='', port=''
1810
    ):
1811
        """ Adds an error result to scan_id scan. """
1812
        self.scan_collection.add_result(
1813
            scan_id, ResultType.ERROR, host, hostname, name, value, port
1814
        )
1815
1816
    def add_scan_host_detail(
1817
        self, scan_id, host='', hostname='', name='', value=''
1818
    ):
1819
        """ Adds a host detail result to scan_id scan. """
1820
        self.scan_collection.add_result(
1821
            scan_id, ResultType.HOST_DETAIL, host, hostname, name, value
1822
        )
1823
1824
    def add_scan_alarm(
1825
        self,
1826
        scan_id,
1827
        host='',
1828
        hostname='',
1829
        name='',
1830
        value='',
1831
        port='',
1832
        test_id='',
1833
        severity='',
1834
        qod='',
1835
    ):
1836
        """ Adds an alarm result to scan_id scan. """
1837
        self.scan_collection.add_result(
1838
            scan_id,
1839
            ResultType.ALARM,
1840
            host,
1841
            hostname,
1842
            name,
1843
            value,
1844
            port,
1845
            test_id,
1846
            severity,
1847
            qod,
1848
        )
1849