Completed
Push — master ( 5762c0...bae074 )
by
unknown
15s queued 11s
created

ospd.ospd.OSPDaemon.run_server()   A

Complexity

Conditions 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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