Passed
Pull Request — master (#131)
by Juan José
02:17
created

ospd.ospd.OSPDaemon.handle_get_performance()   C

Complexity

Conditions 9

Size

Total Lines 54
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 40
nop 2
dl 0
loc 54
rs 6.5866
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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